diff --git a/.gitignore b/.gitignore index 641f731fe03..5d4c43e3fe4 100755 --- a/.gitignore +++ b/.gitignore @@ -34,3 +34,6 @@ envDSLTree test/zstack-integration-test-result/ premium/test-premium/zstack-api.log **/bin/ +CLAUDE.md +.claude/* +.m2/ diff --git a/VERSION b/VERSION index f36c3dfd747..8fc74fe766b 100755 --- a/VERSION +++ b/VERSION @@ -1,3 +1,3 @@ MAJOR=5 MINOR=5 -UPDATE=6 +UPDATE=16 diff --git a/compute/src/main/java/org/zstack/compute/allocator/DesignatedHostAllocatorFlow.java b/compute/src/main/java/org/zstack/compute/allocator/DesignatedHostAllocatorFlow.java index 72386b3dd3e..f6d721ebb8c 100755 --- a/compute/src/main/java/org/zstack/compute/allocator/DesignatedHostAllocatorFlow.java +++ b/compute/src/main/java/org/zstack/compute/allocator/DesignatedHostAllocatorFlow.java @@ -1,6 +1,7 @@ package org.zstack.compute.allocator; import org.apache.commons.collections.CollectionUtils; +import org.apache.commons.lang.StringUtils; import org.springframework.beans.factory.annotation.Autowire; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Configurable; @@ -81,15 +82,29 @@ public void allocate() { List clusterUuids = (List) spec.getExtraData().get(HostAllocatorConstant.LocationSelector.cluster); String hostUuid = (String) spec.getExtraData().get(HostAllocatorConstant.LocationSelector.host); - if (zoneUuid == null && CollectionUtils.isEmpty(clusterUuids) && hostUuid == null && spec.getHypervisorType() == null) { + String hypervisorType = spec.getHypervisorType(); + + // normalize empty strings to null — treat empty string as "not specified" + zoneUuid = StringUtils.isEmpty(zoneUuid) ? null : zoneUuid; + hostUuid = StringUtils.isEmpty(hostUuid) ? null : hostUuid; + hypervisorType = StringUtils.isEmpty(hypervisorType) ? null : hypervisorType; + if (!CollectionUtils.isEmpty(clusterUuids)) { + clusterUuids = new ArrayList<>(clusterUuids); + clusterUuids.removeIf(s -> s == null || s.isEmpty()); + if (clusterUuids.isEmpty()) { + clusterUuids = null; + } + } + + if (zoneUuid == null && CollectionUtils.isEmpty(clusterUuids) && hostUuid == null && hypervisorType == null) { next(candidates); return; } if (amITheFirstFlow()) { - candidates = allocate(zoneUuid, clusterUuids, hostUuid, spec.getHypervisorType()); + candidates = allocate(zoneUuid, clusterUuids, hostUuid, hypervisorType); } else { - candidates = allocate(candidates, zoneUuid, clusterUuids, hostUuid, spec.getHypervisorType()); + candidates = allocate(candidates, zoneUuid, clusterUuids, hostUuid, hypervisorType); } if (candidates.isEmpty()) { @@ -97,14 +112,14 @@ public void allocate() { if (zoneUuid != null) { args.append(String.format("zoneUuid=%s", zoneUuid)).append(" "); } - if (!clusterUuids.isEmpty()) { + if (!CollectionUtils.isEmpty(clusterUuids)) { args.append(String.format("clusterUuid in %s", clusterUuids)).append(" "); } if (hostUuid != null) { args.append(String.format("hostUuid=%s", hostUuid)).append(" "); } - if (spec.getHypervisorType() != null) { - args.append(String.format("hypervisorType=%s", spec.getHypervisorType())).append(" "); + if (hypervisorType != null) { + args.append(String.format("hypervisorType=%s", hypervisorType)).append(" "); } fail(Platform.operr(ORG_ZSTACK_COMPUTE_ALLOCATOR_10036, "No host with %s found", args)); } else { diff --git a/compute/src/main/java/org/zstack/compute/allocator/QuotaAllocatorFlow.java b/compute/src/main/java/org/zstack/compute/allocator/QuotaAllocatorFlow.java index 4d9d469c796..999e797a8da 100644 --- a/compute/src/main/java/org/zstack/compute/allocator/QuotaAllocatorFlow.java +++ b/compute/src/main/java/org/zstack/compute/allocator/QuotaAllocatorFlow.java @@ -4,6 +4,8 @@ import org.springframework.beans.factory.annotation.Configurable; import org.zstack.compute.vm.VmQuotaOperator; import org.zstack.header.allocator.AbstractHostAllocatorFlow; +import org.zstack.header.errorcode.ErrorCode; +import org.zstack.header.errorcode.OperationFailureException; import org.zstack.header.identity.AccountConstant; import org.zstack.identity.Account; import org.zstack.identity.QuotaUtil; @@ -51,6 +53,12 @@ public void allocate() { } throwExceptionIfIAmTheFirstFlow(); + // skip checkquota if the operator is admin + String currentAccountUuid = spec.getAccountUuid(); + if (currentAccountUuid != null && AccountConstant.isAdminPermission(currentAccountUuid)) { + next(candidates); + return; + } final String vmInstanceUuid = spec.getVmInstance().getUuid(); final String accountUuid = Account.getAccountUuidOfResource(vmInstanceUuid); @@ -60,21 +68,27 @@ public void allocate() { } if (!spec.isFullAllocate()) { - new VmQuotaOperator().checkVmCupAndMemoryCapacity(accountUuid, + ErrorCode error = new VmQuotaOperator().checkVmCupAndMemoryCapacityWithResult(accountUuid, accountUuid, spec.getCpuCapacity(), spec.getMemoryCapacity(), new QuotaUtil().makeQuotaPairs(accountUuid)); + if (error != null) { + throw new OperationFailureException(error); + } next(candidates); return; } - new VmQuotaOperator().checkVmInstanceQuota( + ErrorCode error = new VmQuotaOperator().checkVmInstanceQuotaWithResult( accountUuid, accountUuid, vmInstanceUuid, new QuotaUtil().makeQuotaPairs(accountUuid)); + if (error != null) { + throw new OperationFailureException(error); + } next(candidates); } } diff --git a/compute/src/main/java/org/zstack/compute/host/HostManagerImpl.java b/compute/src/main/java/org/zstack/compute/host/HostManagerImpl.java index a15ac71e227..3e478c319e8 100755 --- a/compute/src/main/java/org/zstack/compute/host/HostManagerImpl.java +++ b/compute/src/main/java/org/zstack/compute/host/HostManagerImpl.java @@ -102,6 +102,8 @@ public class HostManagerImpl extends AbstractService implements HostManager, Man private Future reportHostCapacityTask; private Future refreshHostPowerStatusTask; + private static final List SKIP_ARCH_CHECK_HYPERVISOR_TYPES = Arrays.asList("baremetal2", "baremetal2Dpu"); + static { allowedMessageAfterSoftDeletion.add(HostDeletionMsg.class); } @@ -472,7 +474,7 @@ public void run(MessageReply reply) { @Override public boolean skip(Map data) { // no need to check baremetal2 gateway architecture with the cluster architecture - return vo.getHypervisorType().equals("baremetal2"); + return SKIP_ARCH_CHECK_HYPERVISOR_TYPES.contains(cluster.getHypervisorType()); } @Override diff --git a/compute/src/main/java/org/zstack/compute/vm/MigrateVmLongJob.java b/compute/src/main/java/org/zstack/compute/vm/MigrateVmLongJob.java index bd90e4c597a..8a2073333f4 100644 --- a/compute/src/main/java/org/zstack/compute/vm/MigrateVmLongJob.java +++ b/compute/src/main/java/org/zstack/compute/vm/MigrateVmLongJob.java @@ -1,27 +1,34 @@ package org.zstack.compute.vm; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; import org.apache.logging.log4j.ThreadContext; import org.springframework.beans.factory.annotation.Autowire; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Configurable; -import org.zstack.core.Platform; import org.zstack.core.cloudbus.CloudBus; import org.zstack.core.cloudbus.CloudBusCallBack; import org.zstack.core.db.DatabaseFacade; +import org.zstack.core.db.Q; +import org.zstack.core.thread.ThreadFacade; import org.zstack.header.Constants; import org.zstack.header.core.Completion; import org.zstack.header.core.ReturnValueCompletion; -import org.zstack.header.longjob.LongJobErrors; -import org.zstack.header.longjob.LongJobFor; -import org.zstack.header.longjob.LongJobVO; +import org.zstack.header.errorcode.ErrorCode; +import org.zstack.header.longjob.*; import org.zstack.header.message.APIEvent; import org.zstack.header.message.MessageReply; import org.zstack.header.vm.*; -import org.zstack.header.longjob.LongJob; -import org.zstack.longjob.LongJobUtils; +import org.zstack.header.volume.GetVolumeTaskMsg; +import org.zstack.header.volume.GetVolumeTaskReply; +import org.zstack.header.volume.VolumeConstant; +import org.zstack.header.volume.VolumeVO; +import org.zstack.header.volume.VolumeVO_; import org.zstack.utils.gson.JSONObjectUtil; -import static org.zstack.core.Platform.err; +import java.util.*; +import java.util.concurrent.TimeUnit; + import static org.zstack.core.Platform.operr; @@ -31,16 +38,152 @@ @LongJobFor(APIMigrateVmMsg.class) @Configurable(preConstruction = true, autowire = Autowire.BY_TYPE) public class MigrateVmLongJob implements LongJob { + private static final Logger logger = LogManager.getLogger(MigrateVmLongJob.class); + private static final int WAIT_CHAIN_TASK_EXIT_MAX_RETRIES = 30; + private static final long WAIT_CHAIN_TASK_EXIT_INTERVAL_SECS = 1; + @Autowired protected CloudBus bus; @Autowired protected DatabaseFacade dbf; + @Autowired + private ThreadFacade thdf; protected String auditResourceUuid; @Override public void start(LongJobVO job, ReturnValueCompletion completion) { MigrateVmInnerMsg msg = JSONObjectUtil.toObject(job.getJobData(), MigrateVmInnerMsg.class); + + List backupTaskLongJobUuids = getBackupTaskLongJobUuids(job.getJobData()); + if (backupTaskLongJobUuids != null && !backupTaskLongJobUuids.isEmpty()) { + logger.info(String.format("migrate vm[uuid:%s] longjob has %d backup longjobs to cancel first", + msg.getVmInstanceUuid(), backupTaskLongJobUuids.size())); + cancelBackupLongJobsThenMigrate(backupTaskLongJobUuids, msg, completion); + } else { + doMigrate(msg, completion); + } + } + + private List getBackupTaskLongJobUuids(String jobData) { + Map raw = JSONObjectUtil.toObject(jobData, LinkedHashMap.class); + Object uuids = raw == null ? null : raw.get("backupTaskLongJobUuids"); + if (!(uuids instanceof List)) { + return null; + } + + List result = new ArrayList<>(); + for (Object item : (List) uuids) { + if (item == null) { + continue; + } + String uuid = String.valueOf(item).trim(); + if (!uuid.isEmpty()) { + result.add(uuid); + } + } + return result.isEmpty() ? null : result; + } + + private void cancelBackupLongJobsThenMigrate(List backupTaskLongJobUuids, + MigrateVmInnerMsg msg, + ReturnValueCompletion completion) { + cancelBackupLongJobs(backupTaskLongJobUuids.iterator(), new Completion(completion) { + @Override + public void success() { + waitForVolumeChainTasksExit(msg.getVmInstanceUuid(), WAIT_CHAIN_TASK_EXIT_MAX_RETRIES, + new Completion(completion) { + @Override + public void success() { + doMigrate(msg, completion); + } + + @Override + public void fail(ErrorCode errorCode) { + completion.fail(errorCode); + } + }); + } + + @Override + public void fail(ErrorCode errorCode) { + logger.warn(String.format("failed to cancel backup longjobs for vm[uuid:%s], " + + "attempting migration anyway: %s", msg.getVmInstanceUuid(), errorCode)); + doMigrate(msg, completion); + } + }); + } + + private void cancelBackupLongJobs(Iterator it, Completion completion) { + if (!it.hasNext()) { + completion.success(); + return; + } + + String longJobUuid = it.next(); + CancelLongJobMsg cmsg = new CancelLongJobMsg(); + cmsg.setUuid(longJobUuid); + bus.makeLocalServiceId(cmsg, LongJobConstants.SERVICE_ID); + bus.send(cmsg, new CloudBusCallBack(completion) { + @Override + public void run(MessageReply reply) { + if (!reply.isSuccess()) { + logger.warn(String.format("failed to cancel backup longjob[uuid:%s]: %s", + longJobUuid, reply.getError())); + } + cancelBackupLongJobs(it, completion); + } + }); + } + + private void waitForVolumeChainTasksExit(String vmUuid, int retriesLeft, Completion completion) { + List volUuids = Q.New(VolumeVO.class) + .eq(VolumeVO_.vmInstanceUuid, vmUuid) + .select(VolumeVO_.uuid) + .listValues(); + + if (volUuids.isEmpty()) { + completion.success(); + return; + } + + GetVolumeTaskMsg gmsg = new GetVolumeTaskMsg(); + gmsg.setVolumeUuids(volUuids); + bus.makeLocalServiceId(gmsg, VolumeConstant.SERVICE_ID); + bus.send(gmsg, new CloudBusCallBack(completion) { + @Override + public void run(MessageReply reply) { + if (!reply.isSuccess()) { + completion.fail(reply.getError()); + return; + } + + GetVolumeTaskReply gr = reply.castReply(); + boolean hasRunningTasks = gr.getResults().values().stream() + .anyMatch(info -> !info.getRunningTask().isEmpty()); + + if (!hasRunningTasks) { + completion.success(); + return; + } + + if (retriesLeft <= 0) { + completion.fail(operr( + "timeout waiting for volume backup chain tasks to exit for vm[uuid:%s]", vmUuid)); + return; + } + + logger.debug(String.format( + "volumes of vm[uuid:%s] still have running tasks, retry in %ds (retries left: %d)", + vmUuid, WAIT_CHAIN_TASK_EXIT_INTERVAL_SECS, retriesLeft)); + thdf.submitTimeoutTask( + () -> waitForVolumeChainTasksExit(vmUuid, retriesLeft - 1, completion), + TimeUnit.SECONDS, WAIT_CHAIN_TASK_EXIT_INTERVAL_SECS); + } + }); + } + + private void doMigrate(MigrateVmInnerMsg msg, ReturnValueCompletion completion) { bus.makeTargetServiceIdByResourceUuid(msg, VmInstanceConstant.SERVICE_ID, msg.getVmInstanceUuid()); bus.send(msg, new CloudBusCallBack(completion) { @Override diff --git a/compute/src/main/java/org/zstack/compute/vm/StaticIpOperator.java b/compute/src/main/java/org/zstack/compute/vm/StaticIpOperator.java index 86ca327ae93..92bb139450c 100755 --- a/compute/src/main/java/org/zstack/compute/vm/StaticIpOperator.java +++ b/compute/src/main/java/org/zstack/compute/vm/StaticIpOperator.java @@ -1,6 +1,7 @@ package org.zstack.compute.vm; import org.apache.commons.lang.StringUtils; +import org.apache.commons.net.util.SubnetUtils; import org.springframework.beans.factory.annotation.Autowire; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Configurable; @@ -20,6 +21,7 @@ import org.zstack.header.tag.SystemTagValidator; import org.zstack.header.vm.VmInstanceVO; import org.zstack.header.vm.VmNicVO; +import org.zstack.network.l3.IpRangeHelper; import org.zstack.tag.SystemTagCreator; import org.zstack.tag.TagManager; import org.zstack.utils.TagUtils; @@ -80,11 +82,8 @@ public Map getNicNetworkInfoBySystemTag(List s if(VmSystemTags.STATIC_IP.isMatch(sysTag)) { Map token = TagUtils.parse(VmSystemTags.STATIC_IP.getTagFormat(), sysTag); String l3Uuid = token.get(VmSystemTags.STATIC_IP_L3_UUID_TOKEN); - NicIpAddressInfo nicIpAddressInfo = ret.get(l3Uuid); - if (nicIpAddressInfo == null) { - ret.put(l3Uuid, new NicIpAddressInfo("", "", "", - "", "", "")); - } + ret.computeIfAbsent(l3Uuid, k -> new NicIpAddressInfo("", "", "", + "", "", "")); String ip = token.get(VmSystemTags.STATIC_IP_TOKEN); ip = IPv6NetworkUtils.ipv6TagValueToAddress(ip); if (NetworkUtils.isIpv4Address(ip)) { @@ -109,30 +108,42 @@ public Map getNicNetworkInfoBySystemTag(List s continue; } ret.get(l3Uuid).ipv4Gateway = token.get(VmSystemTags.IPV4_GATEWAY_TOKEN); - } - if(VmSystemTags.IPV4_NETMASK.isMatch(sysTag)) { + } else if(VmSystemTags.IPV4_NETMASK.isMatch(sysTag)) { Map token = TagUtils.parse(VmSystemTags.IPV4_NETMASK.getTagFormat(), sysTag); String l3Uuid = token.get(VmSystemTags.IPV4_NETMASK_L3_UUID_TOKEN); if (ret.get(l3Uuid) == null) { continue; } ret.get(l3Uuid).ipv4Netmask = token.get(VmSystemTags.IPV4_NETMASK_TOKEN); - } - if(VmSystemTags.IPV6_GATEWAY.isMatch(sysTag)) { + } else if(VmSystemTags.IPV6_GATEWAY.isMatch(sysTag)) { Map token = TagUtils.parse(VmSystemTags.IPV6_GATEWAY.getTagFormat(), sysTag); String l3Uuid = token.get(VmSystemTags.IPV6_GATEWAY_L3_UUID_TOKEN); if (ret.get(l3Uuid) == null) { continue; } ret.get(l3Uuid).ipv6Gateway = IPv6NetworkUtils.ipv6TagValueToAddress(token.get(VmSystemTags.IPV6_GATEWAY_TOKEN)); - } - if(VmSystemTags.IPV6_PREFIX.isMatch(sysTag)) { + } else if(VmSystemTags.IPV6_PREFIX.isMatch(sysTag)) { Map token = TagUtils.parse(VmSystemTags.IPV6_PREFIX.getTagFormat(), sysTag); String l3Uuid = token.get(VmSystemTags.IPV6_PREFIX_L3_UUID_TOKEN); if (ret.get(l3Uuid) == null) { continue; } ret.get(l3Uuid).ipv6Prefix = token.get(VmSystemTags.IPV6_PREFIX_TOKEN); + } else if(VmSystemTags.STATIC_DNS.isMatch(sysTag)) { + Map token = TagUtils.parse(VmSystemTags.STATIC_DNS.getTagFormat(), sysTag); + String l3Uuid = token.get(VmSystemTags.STATIC_DNS_L3_UUID_TOKEN); + if (ret.get(l3Uuid) == null) { + continue; + } + String dnsStr = token.get(VmSystemTags.STATIC_DNS_TOKEN); + if (dnsStr != null && !dnsStr.isEmpty()) { + // Convert back from tag value: replace '--' with '::' for IPv6 addresses + List dnsList = new ArrayList<>(); + for (String dns : dnsStr.split(",")) { + dnsList.add(IPv6NetworkUtils.ipv6TagValueToAddress(dns)); + } + ret.get(l3Uuid).dnsAddresses = dnsList; + } } } @@ -222,6 +233,49 @@ public void deleteStaticIpByL3NetworkUuid(String l3Uuid) { ))); } + public void setStaticDns(String vmUuid, String l3Uuid, List dnsAddresses) { + if (dnsAddresses == null || dnsAddresses.isEmpty()) { + deleteStaticDnsByVmUuidAndL3Uuid(vmUuid, l3Uuid); + return; + } + + // Convert IPv6 addresses: replace '::' with '--' to avoid conflict with system tag delimiter + List tagSafeDns = new ArrayList<>(); + for (String dns : dnsAddresses) { + tagSafeDns.add(IPv6NetworkUtils.ipv6AddessToTagValue(dns)); + } + String dnsStr = String.join(",", tagSafeDns); + + SimpleQuery q = dbf.createQuery(SystemTagVO.class); + q.select(SystemTagVO_.uuid); + q.add(SystemTagVO_.resourceType, Op.EQ, VmInstanceVO.class.getSimpleName()); + q.add(SystemTagVO_.resourceUuid, Op.EQ, vmUuid); + q.add(SystemTagVO_.tag, Op.LIKE, TagUtils.tagPatternToSqlPattern(VmSystemTags.STATIC_DNS.instantiateTag( + map(e(VmSystemTags.STATIC_DNS_L3_UUID_TOKEN, l3Uuid)) + ))); + String tagUuid = q.findValue(); + + if (tagUuid == null) { + SystemTagCreator creator = VmSystemTags.STATIC_DNS.newSystemTagCreator(vmUuid); + creator.setTagByTokens(map( + e(VmSystemTags.STATIC_DNS_L3_UUID_TOKEN, l3Uuid), + e(VmSystemTags.STATIC_DNS_TOKEN, dnsStr) + )); + creator.create(); + } else { + VmSystemTags.STATIC_DNS.updateByTagUuid(tagUuid, VmSystemTags.STATIC_DNS.instantiateTag(map( + e(VmSystemTags.STATIC_DNS_L3_UUID_TOKEN, l3Uuid), + e(VmSystemTags.STATIC_DNS_TOKEN, dnsStr) + ))); + } + } + + public void deleteStaticDnsByVmUuidAndL3Uuid(String vmUuid, String l3Uuid) { + VmSystemTags.STATIC_DNS.delete(vmUuid, TagUtils.tagPatternToSqlPattern(VmSystemTags.STATIC_DNS.instantiateTag( + map(e(VmSystemTags.STATIC_DNS_L3_UUID_TOKEN, l3Uuid)) + ))); + } + public Map getNicStaticIpMap(List nicStaticIpList) { Map nicStaticIpMap = new HashMap<>(); if (nicStaticIpList != null) { @@ -263,14 +317,14 @@ public boolean isIpChange(String vmUuid, String l3Uuid) { return false; } - public Boolean checkIpRangeConflict(VmNicVO nicVO){ + public Boolean isNicIpInL3IpRanges(VmNicVO nicVO){ if (Q.New(IpRangeVO.class).eq(IpRangeVO_.l3NetworkUuid, nicVO.getL3NetworkUuid()).list().isEmpty()) { - return Boolean.FALSE; + return Boolean.TRUE; } if (getIpRangeUuid(nicVO.getL3NetworkUuid(), nicVO.getIp()) == null) { - return Boolean.TRUE; + return Boolean.FALSE; } - return Boolean.FALSE; + return Boolean.TRUE; } public String getIpRangeUuid(String l3Uuid, String ip) { @@ -297,10 +351,19 @@ public String getIpRangeUuid(String l3Uuid, String ip) { return null; } + public NormalIpRangeVO findMatchedNormalIpRange(String l3Uuid, String ip) { + String rangeUuid = getIpRangeUuid(l3Uuid, ip); + if (rangeUuid == null) { + return null; + } + return dbf.findByUuid(rangeUuid, NormalIpRangeVO.class); + } + public void checkIpAvailability(String l3Uuid, String ip) { CheckIpAvailabilityMsg cmsg = new CheckIpAvailabilityMsg(); cmsg.setIp(ip); cmsg.setL3NetworkUuid(l3Uuid); + cmsg.setIpRangeCheck(false); bus.makeLocalServiceId(cmsg, L3NetworkConstant.SERVICE_ID); MessageReply r = bus.call(cmsg); if (!r.isSuccess()) { @@ -319,8 +382,223 @@ public void validateSystemTagInCreateMessage(APICreateMessage msg) { validateSystemTagInApiMessage(msg); } - public List fillUpStaticIpInfoToVmNics(Map staticIps) { - List newSystags = new ArrayList<>(); + // ================================================================ + // Context classes for unified resolve logic + // ================================================================ + + /** + * Describes the role of the NIC being resolved, used by resolve methods + * to decide whether gateway is mandatory. + */ + public static class NicRoleContext { + public final boolean isDefaultNic; + public final boolean isOnlyNic; + + public NicRoleContext(boolean isDefaultNic, boolean isOnlyNic) { + this.isDefaultNic = isDefaultNic; + this.isOnlyNic = isOnlyNic; + } + } + + /** + * Holds existing UsedIpVO per (l3Uuid, ipVersion) for case(d) reuse logic. + * Only APISetVmStaticIpMsg populates this; APIChangeVmNicNetworkMsg passes empty. + */ + public static class ExistingIpContext { + private final Map ipv4Map = new HashMap<>(); + private final Map ipv6Map = new HashMap<>(); + + public void putIpv4(String l3Uuid, UsedIpVO vo) { + if (vo != null) { + ipv4Map.put(l3Uuid, vo); + } + } + + public void putIpv6(String l3Uuid, UsedIpVO vo) { + if (vo != null) { + ipv6Map.put(l3Uuid, vo); + } + } + + public UsedIpVO getIpv4(String l3Uuid) { + return ipv4Map.get(l3Uuid); + } + + public UsedIpVO getIpv6(String l3Uuid) { + return ipv6Map.get(l3Uuid); + } + } + + // ================================================================ + // Unified resolve methods (migrated from VmInstanceApiInterceptor) + // ================================================================ + + /** + * Determine whether to use the NIC's existing IPv4 parameters (netmask/gateway). + * Condition: existingIp is non-null with non-empty netmask and non-empty gateway, + * and the IP falls within the CIDR formed by existingIp's gateway + netmask. + */ + public boolean shouldUseExistingIpv4(String ip, UsedIpVO existingIp) { + if (existingIp == null || StringUtils.isEmpty(existingIp.getNetmask())) { + return false; + } + if (StringUtils.isEmpty(existingIp.getGateway())) { + return false; + } + try { + SubnetUtils.SubnetInfo info = NetworkUtils.getSubnetInfo( + new SubnetUtils(existingIp.getGateway(), existingIp.getNetmask())); + return NetworkUtils.isIpv4InRange(ip, info.getLowAddress(), info.getHighAddress()); + } catch (Exception e) { + return false; + } + } + + /** + * Determine whether to use the NIC's existing IPv6 parameters (prefix/gateway). + * Condition: existingIp is non-null with non-null prefixLen and non-empty gateway, + * and the IP falls within the CIDR formed by existingIp's gateway + prefixLen. + */ + public boolean shouldUseExistingIpv6(String ip6, UsedIpVO existingIp) { + if (existingIp == null || existingIp.getPrefixLen() == null) { + return false; + } + if (StringUtils.isEmpty(existingIp.getGateway())) { + return false; + } + try { + return IPv6NetworkUtils.isIpv6InCidrRange(ip6, + existingIp.getGateway() + "/" + existingIp.getPrefixLen()); + } catch (Exception e) { + return false; + } + } + + /** + * Resolve IPv4 netmask and gateway based on 4 cases: + * (a) Both netmask+gateway provided: validate gateway is in CIDR(ip/netmask), then use user input + * (b) Gateway provided, no netmask: if ip and gateway both in L3 CIDR, use CIDR netmask; else error + * (c) Netmask provided, no gateway: if netmask == CIDR netmask, use CIDR gateway; else gateway="" + * (d) Neither provided: if existingIp usable, use it; else if in L3 CIDR, use CIDR; else error + */ + public String[] resolveIpv4NetmaskAndGateway(String ip, String userNetmask, String userGateway, + List ipv4Ranges, NicRoleContext nicRole, UsedIpVO existingIp) { + boolean hasNetmask = StringUtils.isNotEmpty(userNetmask); + boolean hasGateway = StringUtils.isNotEmpty(userGateway); + + // case (a): both provided — validate gateway is in the CIDR formed by ip/netmask + if (hasNetmask && hasGateway) { + String cidr = NetworkUtils.getCidrFromIpMask(ip, userNetmask); + if (!NetworkUtils.isIpv4InCidr(userGateway, cidr)) { + throw new ApiMessageInterceptionException(argerr(ORG_ZSTACK_COMPUTE_VM_10329, + "gateway[%s] is not in the CIDR[%s] formed by IP[%s] and netmask[%s]", + userGateway, cidr, ip, userNetmask)); + } + return new String[]{userNetmask, userGateway}; + } + + NormalIpRangeVO matchedRange = IpRangeHelper.findIpRangeByCidr(ip, ipv4Ranges); + + // case (b): gateway provided, no netmask + if (hasGateway) { + if (matchedRange != null && matchedRange.getNetworkCidr() != null + && NetworkUtils.isIpv4InCidr(userGateway, matchedRange.getNetworkCidr())) { + return new String[]{matchedRange.getNetmask(), userGateway}; + } + throw new ApiMessageInterceptionException(argerr(ORG_ZSTACK_COMPUTE_VM_10323, + "gateway[%s] is provided but IP[%s] and gateway are not both in L3 network CIDR, netmask must be specified", + userGateway, ip)); + } + + // case (c): netmask provided, no gateway + if (hasNetmask) { + if (matchedRange != null && userNetmask.equals(matchedRange.getNetmask())) { + return new String[]{matchedRange.getNetmask(), matchedRange.getGateway()}; + } + return new String[]{userNetmask, ""}; + } + + // case (d): neither provided + if (existingIp != null && shouldUseExistingIpv4(ip, existingIp)) { + return new String[]{existingIp.getNetmask(), existingIp.getGateway()}; + } + if (matchedRange != null) { + return new String[]{matchedRange.getNetmask(), matchedRange.getGateway()}; + } + throw new ApiMessageInterceptionException(argerr(ORG_ZSTACK_COMPUTE_VM_10325, + "IP[%s] is outside all L3 network CIDRs and no existing IP parameters available, netmask and gateway must be specified", + ip)); + } + + /** + * Resolve IPv6 prefix and gateway based on 4 cases (mirrors IPv4 logic): + * (a) Both prefix+gateway provided: validate gateway is in CIDR(ip6/prefix), then use user input + * (b) Gateway provided, no prefix: if ip and gateway both in L3 CIDR, use CIDR prefix; else error + * (c) Prefix provided, no gateway: if prefix == CIDR prefix, use CIDR gateway; else if default/sole NIC, error; else gateway="" + * (d) Neither provided: if existingIp usable, use it; else if in L3 CIDR, use CIDR; else error + */ + public String[] resolveIpv6PrefixAndGateway(String ip6, String userPrefix, String userGateway, + List ipv6Ranges, NicRoleContext nicRole, UsedIpVO existingIp) { + boolean hasPrefix = StringUtils.isNotEmpty(userPrefix); + boolean hasGateway = StringUtils.isNotEmpty(userGateway); + + // case (a): both provided — validate gateway is in the CIDR formed by ip6/prefix + if (hasPrefix && hasGateway) { + String cidr = ip6 + "/" + userPrefix; + if (!IPv6NetworkUtils.isIpv6InCidrRange(userGateway, cidr)) { + throw new ApiMessageInterceptionException(argerr(ORG_ZSTACK_COMPUTE_VM_10330, + "gateway[%s] is not in the CIDR[%s] formed by IPv6[%s] and prefix[%s]", + userGateway, cidr, ip6, userPrefix)); + } + return new String[]{userPrefix, userGateway}; + } + + NormalIpRangeVO matchedRange = IpRangeHelper.findIpRangeByCidr(ip6, ipv6Ranges); + + // case (b): gateway provided, no prefix + if (hasGateway) { + if (matchedRange != null && matchedRange.getNetworkCidr() != null + && IPv6NetworkUtils.isIpv6InCidrRange(userGateway, matchedRange.getNetworkCidr())) { + return new String[]{matchedRange.getPrefixLen().toString(), userGateway}; + } + throw new ApiMessageInterceptionException(argerr(ORG_ZSTACK_COMPUTE_VM_10326, + "gateway[%s] is provided but IPv6[%s] and gateway are not both in L3 network CIDR, prefix must be specified", + userGateway, ip6)); + } + + // case (c): prefix provided, no gateway + if (hasPrefix) { + if (matchedRange != null && userPrefix.equals(matchedRange.getPrefixLen().toString())) { + return new String[]{matchedRange.getPrefixLen().toString(), matchedRange.getGateway()}; + } + if (nicRole.isDefaultNic || nicRole.isOnlyNic) { + throw new ApiMessageInterceptionException(argerr(ORG_ZSTACK_COMPUTE_VM_10327, + "prefix[%s] does not match L3 CIDR prefix and the NIC is the default or sole network, gateway must be specified", + userPrefix)); + } + return new String[]{userPrefix, ""}; + } + + // case (d): neither provided + if (existingIp != null && shouldUseExistingIpv6(ip6, existingIp)) { + return new String[]{existingIp.getPrefixLen().toString(), existingIp.getGateway()}; + } + if (matchedRange != null) { + return new String[]{matchedRange.getPrefixLen().toString(), matchedRange.getGateway()}; + } + throw new ApiMessageInterceptionException(argerr(ORG_ZSTACK_COMPUTE_VM_10328, + "IPv6[%s] is outside all L3 network CIDRs and no existing IP parameters available, prefix and gateway must be specified", + ip6)); + } + + // ================================================================ + // IP availability validation (extracted from old fillUpStaticIpInfoToVmNics) + // ================================================================ + + /** + * Validate that all static IPs are available on their respective L3 networks. + */ + public void validateIpAvailability(Map staticIps) { for (Map.Entry e : staticIps.entrySet()) { String l3Uuid = e.getKey(); NicIpAddressInfo nicIp = e.getValue(); @@ -332,69 +610,63 @@ public List fillUpStaticIpInfoToVmNics(Map sta if (!StringUtils.isEmpty(nicIp.ipv6Address)) { checkIpAvailability(l3Uuid, nicIp.ipv6Address); } + } + } + // ================================================================ + // fillUpStaticIpInfoToVmNics — orchestration layer + // ================================================================ + + /** + * New signature: resolves netmask/gateway (or prefix/gateway) for each static IP entry, + * using the unified resolve methods with NicRoleContext and ExistingIpContext. + * Returns system tags to be added to the message. + */ + public List fillUpStaticIpInfoToVmNics(Map staticIps, + NicRoleContext nicRole, ExistingIpContext existingIpCtx) { + List newSystags = new ArrayList<>(); + for (Map.Entry entry : staticIps.entrySet()) { + String l3Uuid = entry.getKey(); + NicIpAddressInfo nicIp = entry.getValue(); + + // Resolve IPv4 netmask/gateway if (!StringUtils.isEmpty(nicIp.ipv4Address)) { - NormalIpRangeVO ipRangeVO = Q.New(NormalIpRangeVO.class) + List ipv4Ranges = Q.New(NormalIpRangeVO.class) .eq(NormalIpRangeVO_.l3NetworkUuid, l3Uuid) - .eq(NormalIpRangeVO_.ipVersion, IPv6Constants.IPv4) - .limit(1).find(); - if (ipRangeVO == null) { - if (StringUtils.isEmpty(nicIp.ipv4Netmask)) { - throw new ApiMessageInterceptionException(operr(ORG_ZSTACK_COMPUTE_VM_10310, "netmask must be set")); - } - } else { - if (StringUtils.isEmpty(nicIp.ipv4Netmask)) { - newSystags.add(VmSystemTags.IPV4_NETMASK.instantiateTag( - map(e(VmSystemTags.IPV4_NETMASK_L3_UUID_TOKEN, l3Uuid), - e(VmSystemTags.IPV4_NETMASK_TOKEN, ipRangeVO.getNetmask())) - )); - } else if (!nicIp.ipv4Netmask.equals(ipRangeVO.getNetmask())) { - throw new ApiMessageInterceptionException(operr(ORG_ZSTACK_COMPUTE_VM_10311, "netmask error, expect: %s, got: %s", - ipRangeVO.getNetmask(), nicIp.ipv4Netmask)); - } - - if (StringUtils.isEmpty(nicIp.ipv4Gateway)) { - newSystags.add(VmSystemTags.IPV4_GATEWAY.instantiateTag( - map(e(VmSystemTags.IPV4_GATEWAY_L3_UUID_TOKEN, l3Uuid), - e(VmSystemTags.IPV4_GATEWAY_TOKEN, ipRangeVO.getGateway())) - )); - } else if (!nicIp.ipv4Gateway.equals(ipRangeVO.getGateway())) { - throw new ApiMessageInterceptionException(operr(ORG_ZSTACK_COMPUTE_VM_10312, "gateway error, expect: %s, got: %s", - ipRangeVO.getGateway(), nicIp.ipv4Gateway)); - } + .eq(NormalIpRangeVO_.ipVersion, IPv6Constants.IPv4).list(); + UsedIpVO existingIpv4 = existingIpCtx != null ? existingIpCtx.getIpv4(l3Uuid) : null; + + String[] ipv4Result = resolveIpv4NetmaskAndGateway(nicIp.ipv4Address, + nicIp.ipv4Netmask, nicIp.ipv4Gateway, ipv4Ranges, nicRole, existingIpv4); + + newSystags.add(VmSystemTags.IPV4_NETMASK.instantiateTag( + map(e(VmSystemTags.IPV4_NETMASK_L3_UUID_TOKEN, l3Uuid), + e(VmSystemTags.IPV4_NETMASK_TOKEN, ipv4Result[0])))); + if (!StringUtils.isEmpty(ipv4Result[1])) { + newSystags.add(VmSystemTags.IPV4_GATEWAY.instantiateTag( + map(e(VmSystemTags.IPV4_GATEWAY_L3_UUID_TOKEN, l3Uuid), + e(VmSystemTags.IPV4_GATEWAY_TOKEN, ipv4Result[1])))); } } + // Resolve IPv6 prefix/gateway if (!StringUtils.isEmpty(nicIp.ipv6Address)) { - NormalIpRangeVO ipRangeVO = Q.New(NormalIpRangeVO.class) + List ipv6Ranges = Q.New(NormalIpRangeVO.class) .eq(NormalIpRangeVO_.l3NetworkUuid, l3Uuid) - .eq(NormalIpRangeVO_.ipVersion, IPv6Constants.IPv6) - .limit(1).find(); - if (ipRangeVO == null) { - if (StringUtils.isEmpty(nicIp.ipv6Prefix)) { - throw new ApiMessageInterceptionException(operr(ORG_ZSTACK_COMPUTE_VM_10313, "ipv6 prefix length must be set")); - } - } else { - if (StringUtils.isEmpty(nicIp.ipv6Prefix)) { - newSystags.add(VmSystemTags.IPV6_PREFIX.instantiateTag( - map(e(VmSystemTags.IPV6_PREFIX_L3_UUID_TOKEN, l3Uuid), - e(VmSystemTags.IPV6_PREFIX_TOKEN, ipRangeVO.getPrefixLen())) - )); - } else if (!nicIp.ipv6Prefix.equals(ipRangeVO.getPrefixLen().toString())) { - throw new ApiMessageInterceptionException(operr(ORG_ZSTACK_COMPUTE_VM_10314, "ipv6 prefix length error, expect: %s, got: %s", - ipRangeVO.getPrefixLen(), nicIp.ipv6Prefix)); - } - - if (StringUtils.isEmpty(nicIp.ipv6Gateway)) { - newSystags.add(VmSystemTags.IPV6_GATEWAY.instantiateTag( - map(e(VmSystemTags.IPV6_GATEWAY_L3_UUID_TOKEN, l3Uuid), - e(VmSystemTags.IPV6_GATEWAY_TOKEN, - IPv6NetworkUtils.ipv6AddressToTagValue(ipRangeVO.getGateway()))) - )); - } else if (!nicIp.ipv6Gateway.equals(ipRangeVO.getGateway())) { - throw new ApiMessageInterceptionException(operr(ORG_ZSTACK_COMPUTE_VM_10315, "gateway error, expect: %s, got: %s", - ipRangeVO.getGateway(), nicIp.ipv6Gateway)); - } + .eq(NormalIpRangeVO_.ipVersion, IPv6Constants.IPv6).list(); + UsedIpVO existingIpv6 = existingIpCtx != null ? existingIpCtx.getIpv6(l3Uuid) : null; + + String[] ipv6Result = resolveIpv6PrefixAndGateway(nicIp.ipv6Address, + nicIp.ipv6Prefix, nicIp.ipv6Gateway, ipv6Ranges, nicRole, existingIpv6); + + newSystags.add(VmSystemTags.IPV6_PREFIX.instantiateTag( + map(e(VmSystemTags.IPV6_PREFIX_L3_UUID_TOKEN, l3Uuid), + e(VmSystemTags.IPV6_PREFIX_TOKEN, ipv6Result[0])))); + if (!StringUtils.isEmpty(ipv6Result[1])) { + newSystags.add(VmSystemTags.IPV6_GATEWAY.instantiateTag( + map(e(VmSystemTags.IPV6_GATEWAY_L3_UUID_TOKEN, l3Uuid), + e(VmSystemTags.IPV6_GATEWAY_TOKEN, + IPv6NetworkUtils.ipv6AddressToTagValue(ipv6Result[1]))))); } } } @@ -402,10 +674,27 @@ public List fillUpStaticIpInfoToVmNics(Map sta return newSystags; } + /** + * Legacy overload: preserves old behavior for existing callers + * (APICreateVmInstanceMsg, APIAttachL3NetworkToVmMsg). + * Uses default NicRoleContext(false, false) and empty ExistingIpContext. + */ + public List fillUpStaticIpInfoToVmNics(Map staticIps) { + return fillUpStaticIpInfoToVmNics(staticIps, + new NicRoleContext(false, false), new ExistingIpContext()); + } + public void validateSystemTagInApiMessage(APIMessage msg) { Map staticIps = getNicNetworkInfoBySystemTag(msg.getSystemTags()); + validateIpAvailability(staticIps); List newSystags = fillUpStaticIpInfoToVmNics(staticIps); if (!newSystags.isEmpty()) { + if (msg.getSystemTags() != null) { + // Remove any existing netmask/gateway/prefix tags before adding resolved ones + msg.getSystemTags().removeIf(tag -> + VmSystemTags.IPV4_NETMASK.isMatch(tag) || VmSystemTags.IPV4_GATEWAY.isMatch(tag) + || VmSystemTags.IPV6_PREFIX.isMatch(tag) || VmSystemTags.IPV6_GATEWAY.isMatch(tag)); + } msg.getSystemTags().addAll(newSystags); } } diff --git a/compute/src/main/java/org/zstack/compute/vm/VmCascadeExtension.java b/compute/src/main/java/org/zstack/compute/vm/VmCascadeExtension.java index 5b2dd2f399a..9bd98bd9511 100755 --- a/compute/src/main/java/org/zstack/compute/vm/VmCascadeExtension.java +++ b/compute/src/main/java/org/zstack/compute/vm/VmCascadeExtension.java @@ -300,8 +300,8 @@ protected List handleDeletionForIpRange(List handleDeletionForIpRange(List ips, String l3NetworkUuid) { + if (ips == null || ips.isEmpty()) { + return; + } + + List occupiedIps = Q.New(UsedIpVO.class) + .eq(UsedIpVO_.l3NetworkUuid, l3NetworkUuid) + .in(UsedIpVO_.ip, ips) + .select(UsedIpVO_.ip) + .listValues(); + + if (!occupiedIps.isEmpty()) { + throw new ApiMessageInterceptionException(operr(ORG_ZSTACK_COMPUTE_VM_10105, + "the static IP%s has been occupied on the L3 network[uuid:%s]", occupiedIps, l3NetworkUuid)); + } + } + + private void setServiceId(APIMessage msg) { if (msg instanceof VmInstanceMessage) { VmInstanceMessage vmsg = (VmInstanceMessage) msg; @@ -286,100 +346,81 @@ private void validate(APIChangeVmNicNetworkMsg msg) { } } - new StaticIpOperator().validateSystemTagInApiMessage(msg); - Map> staticIps = new StaticIpOperator().getStaticIpbySystemTag(msg.getSystemTags()); - if (msg.getRequiredIpMap() != null) { - staticIps.computeIfAbsent(msg.getDestL3NetworkUuid(), k -> new ArrayList<>()).add(msg.getStaticIp()); - SimpleQuery iprq = dbf.createQuery(NormalIpRangeVO.class); - iprq.add(NormalIpRangeVO_.l3NetworkUuid, Op.EQ, msg.getDestL3NetworkUuid()); - List iprs = iprq.list(); + // Build NicRoleContext for resolve logic + String defaultL3Uuid = Q.New(VmInstanceVO.class) + .select(VmInstanceVO_.defaultL3NetworkUuid) + .eq(VmInstanceVO_.uuid, vmUuid) + .findValue(); + int vmNicCount = Q.New(VmNicVO.class).eq(VmNicVO_.vmInstanceUuid, vmUuid).count().intValue(); + boolean isDefaultNic = srcL3Uuid.equals(defaultL3Uuid); + boolean isOnlyNic = vmNicCount == 1; - boolean found = false; - for (NormalIpRangeVO ipr : iprs) { - if (!ipr.getIpVersion().equals(NetworkUtils.getIpversion(msg.getStaticIp()))) { - continue; - } + StaticIpOperator staticIpOp = new StaticIpOperator(); + Map nicNetworkInfo = staticIpOp.getNicNetworkInfoBySystemTag(msg.getSystemTags()); - if (NetworkUtils.isInRange(msg.getStaticIp(), ipr.getStartIp(), ipr.getEndIp())) { - found = true; - break; - } - } + // Validate IP availability + staticIpOp.validateIpAvailability(nicNetworkInfo); - if (!l3NetworkVO.enableIpAddressAllocation()) { - found = true; - } + // Resolve netmask/gateway using unified logic (no existingIp reuse for ChangeNicNetwork) + StaticIpOperator.NicRoleContext nicRole = new StaticIpOperator.NicRoleContext(isDefaultNic, isOnlyNic); + List resolvedTags = staticIpOp.fillUpStaticIpInfoToVmNics(nicNetworkInfo, + nicRole, new StaticIpOperator.ExistingIpContext()); - if (!found) { - throw new ApiMessageInterceptionException(argerr(ORG_ZSTACK_COMPUTE_VM_10104, "the static IP[%s] is not in any IP range of the L3 network[uuid:%s]", msg.getStaticIp(), msg.getDestL3NetworkUuid())); - } + // Remove any existing netmask/gateway/prefix tags, then add resolved ones + if (msg.getSystemTags() != null) { + msg.getSystemTags().removeIf(tag -> + VmSystemTags.IPV4_NETMASK.isMatch(tag) || VmSystemTags.IPV4_GATEWAY.isMatch(tag) + || VmSystemTags.IPV6_PREFIX.isMatch(tag) || VmSystemTags.IPV6_GATEWAY.isMatch(tag)); + } + if (!resolvedTags.isEmpty()) { + msg.getSystemTags().addAll(resolvedTags); + } - SimpleQuery uq = dbf.createQuery(UsedIpVO.class); - uq.add(UsedIpVO_.l3NetworkUuid, Op.EQ, msg.getDestL3NetworkUuid()); - uq.add(UsedIpVO_.ip, Op.EQ, msg.getStaticIp()); - if (uq.isExists()) { - throw new ApiMessageInterceptionException(operr(ORG_ZSTACK_COMPUTE_VM_10105, "the static IP[%s] has been occupied on the L3 network[uuid:%s]", msg.getStaticIp(), msg.getDestL3NetworkUuid())); - } + Map> staticIps = new StaticIpOperator().getStaticIpbySystemTag(msg.getSystemTags()); + + // If staticIp parameter is provided, add it to the static IP list + if (msg.getStaticIp() != null) { + staticIps.computeIfAbsent(msg.getDestL3NetworkUuid(), k -> new ArrayList<>()).add(msg.getStaticIp()); } - for (Map.Entry> e : staticIps.entrySet()) { - if (!newAddedL3Uuids.contains(e.getKey())) { - throw new ApiMessageInterceptionException(argerr(ORG_ZSTACK_COMPUTE_VM_10106, "static ip l3 uuid[%s] is not included in nic l3 [%s]", e.getKey(), newAddedL3Uuids)); - } + msg.setRequiredIpMap(new HashMap<>()); + // Unified loop: validate and set all static IPs together + for (Map.Entry> e : staticIps.entrySet()) { String l3Uuid = e.getKey(); List ips = e.getValue(); - SimpleQuery iprq = dbf.createQuery(NormalIpRangeVO.class); - iprq.add(NormalIpRangeVO_.l3NetworkUuid, Op.EQ, l3Uuid); - List iprs = iprq.list(); - - boolean found = false; - for (String staticIp : ips) { - int ipVersion = IPv6Constants.IPv4; - if (IPv6NetworkUtils.isIpv6Address(staticIp)) { - ipVersion = IPv6Constants.IPv6; - } - for (NormalIpRangeVO ipr : iprs) { - if (ipVersion != ipr.getIpVersion()) { - continue; - } - if (NetworkUtils.isInRange(staticIp, ipr.getStartIp(), ipr.getEndIp())) { - found = true; - break; - } - } - if (!l3NetworkVO.enableIpAddressAllocation()) { - found = true; - } + // Validate that the L3 network UUID is in the allowed list + if (!newAddedL3Uuids.contains(l3Uuid)) { + throw new ApiMessageInterceptionException(argerr(ORG_ZSTACK_COMPUTE_VM_10106, + "static ip l3 uuid[%s] is not included in nic l3 [%s]", l3Uuid, newAddedL3Uuids)); + } - if (!found) { - throw new ApiMessageInterceptionException(argerr(ORG_ZSTACK_COMPUTE_VM_10107, "the static IP[%s] is not in any IP range of the L3 network[uuid:%s]", staticIp, l3Uuid)); - } + // Performance optimization: batch check IP occupation (one query instead of N) + checkIpsOccupied(ips, l3Uuid); - SimpleQuery uq = dbf.createQuery(UsedIpVO.class); - uq.add(UsedIpVO_.l3NetworkUuid, Op.EQ, msg.getDestL3NetworkUuid()); - uq.add(UsedIpVO_.ip, Op.EQ, msg.getStaticIp()); - if (uq.isExists()) { - throw new ApiMessageInterceptionException(operr(ORG_ZSTACK_COMPUTE_VM_10108, "the static IP[%s] has been occupied on the L3 network[uuid:%s]", staticIp, l3Uuid)); - } - } + // Set requiredIpMap (merged into this loop to eliminate redundant iteration) + msg.getRequiredIpMap().put(l3Uuid, ips); } - msg.setRequiredIpMap(new HashMap<>()); + validateDnsAddresses(msg.getDnsAddresses()); + } - for (Map.Entry> e : staticIps.entrySet()) { - msg.getRequiredIpMap().put(e.getKey(), e.getValue()); + private void validateDnsAddresses(List dnsAddresses) { + if (dnsAddresses == null || dnsAddresses.isEmpty()) { + return; } - final Map nicNetworkInfo = new StaticIpOperator().getNicNetworkInfoBySystemTag(msg.getSystemTags()); - NicIpAddressInfo nicIpAddressInfo = nicNetworkInfo.get(msg.getDestL3NetworkUuid()); - if (nicIpAddressInfo != null) { - if (!nicIpAddressInfo.ipv4Address.isEmpty() && Q.New(UsedIpVO.class).eq(UsedIpVO_.ip, nicIpAddressInfo.ipv4Address).eq(UsedIpVO_.l3NetworkUuid, msg.getDestL3NetworkUuid()).isExists()) { - throw new ApiMessageInterceptionException(argerr(ORG_ZSTACK_COMPUTE_VM_10109, "the static IP[%s] has been occupied on the L3 network[uuid:%s]", nicIpAddressInfo.ipv4Address, msg.getDestL3NetworkUuid())); - } - if (!nicIpAddressInfo.ipv6Address.isEmpty() && Q.New(UsedIpVO.class).eq(UsedIpVO_.ip, IPv6NetworkUtils.getIpv6AddressCanonicalString(nicIpAddressInfo.ipv6Address)).eq(UsedIpVO_.l3NetworkUuid, msg.getDestL3NetworkUuid()).isExists()) { - throw new ApiMessageInterceptionException(argerr(ORG_ZSTACK_COMPUTE_VM_10110, "the static IP[%s] has been occupied on the L3 network[uuid:%s]", nicIpAddressInfo.ipv6Address, msg.getDestL3NetworkUuid())); + if (dnsAddresses.size() > VmInstanceConstant.MAXIMUM_NIC_DNS_NUMBER) { + throw new ApiMessageInterceptionException(argerr( + ORG_ZSTACK_COMPUTE_VM_10321, "at most %d DNS addresses are allowed, but got %d", + VmInstanceConstant.MAXIMUM_NIC_DNS_NUMBER, dnsAddresses.size())); + } + + for (String dns : dnsAddresses) { + if (!NetworkUtils.isIpv4Address(dns) && !IPv6NetworkUtils.isIpv6Address(dns)) { + throw new ApiMessageInterceptionException(argerr( + ORG_ZSTACK_COMPUTE_VM_10322, "invalid DNS address[%s], must be a valid IPv4 or IPv6 address", dns)); } } } @@ -524,6 +565,7 @@ protected void scripts() { throw new ApiMessageInterceptionException(argerr( ORG_ZSTACK_COMPUTE_VM_10124, "the VM cannot do cpu hot plug because of disabling cpu hot plug. Please stop the VM then do the cpu hot plug again" )); + } if (memorySize != null && memorySize != vo.getMemorySize()) { @@ -570,58 +612,13 @@ private void validate(APIStartVmInstanceMsg msg) { } private void validateStaticIPv4(VmNicVO vmNicVO, L3NetworkVO l3NetworkVO, String ip) { - if (!NetworkUtils.isIpv4Address(ip)) { - throw new ApiMessageInterceptionException(argerr(ORG_ZSTACK_COMPUTE_VM_10129, "%s is not a valid IPv4 address", ip)); - } - - for (UsedIpVO ipVo : vmNicVO.getUsedIps()) { - if (ipVo.getIpVersion() != IPv6Constants.IPv4) { - continue; - } - - if (ipVo.getL3NetworkUuid().equals(l3NetworkVO.getUuid())) { - if (ipVo.getIp().equals(ip)) { - throw new ApiMessageInterceptionException(argerr(ORG_ZSTACK_COMPUTE_VM_10130, "ip address [%s] already set to vmNic [uuid:%s]", - ip, vmNicVO.getUuid())); - } - if (!l3NetworkVO.enableIpAddressAllocation()) { - continue; - } - // check if the ip is in the ip range when ipam is enabled - NormalIpRangeVO rangeVO = dbf.findByUuid(ipVo.getIpRangeUuid(), NormalIpRangeVO.class); - if (!NetworkUtils.isIpv4InCidr(ip, rangeVO.getNetworkCidr())) { - throw new ApiMessageInterceptionException(argerr(ORG_ZSTACK_COMPUTE_VM_10131, "ip address [%s] is not in ip range [%s]", - ip, rangeVO.getNetworkCidr())); - } - } - } + validateStaticIpCommon(vmNicVO, l3NetworkVO, ip, IPv6Constants.IPv4, + ORG_ZSTACK_COMPUTE_VM_10129, ORG_ZSTACK_COMPUTE_VM_10130); } private void validateStaticIPv6(VmNicVO vmNicVO, L3NetworkVO l3NetworkVO, String ip) { - if (!IPv6NetworkUtils.isIpv6Address(ip)) { - throw new ApiMessageInterceptionException(argerr(ORG_ZSTACK_COMPUTE_VM_10132, "%s is not a valid IPv6 address", ip)); - } - - for (UsedIpVO ipVo : vmNicVO.getUsedIps()) { - if (ipVo.getIpVersion() != IPv6Constants.IPv6) { - continue; - } - - if (ipVo.getL3NetworkUuid().equals(l3NetworkVO.getUuid())) { - if (ip.equals(ipVo.getIp())) { - throw new ApiMessageInterceptionException(argerr(ORG_ZSTACK_COMPUTE_VM_10133, "ip address [%s] already set to vmNic [uuid:%s]", - ip, vmNicVO.getUuid())); - } - if (!l3NetworkVO.enableIpAddressAllocation()) { - continue; - } - NormalIpRangeVO rangeVO = dbf.findByUuid(ipVo.getIpRangeUuid(), NormalIpRangeVO.class); - if (!IPv6NetworkUtils.isIpv6InRange(ip, rangeVO.getStartIp(), rangeVO.getEndIp())) { - throw new ApiMessageInterceptionException(argerr(ORG_ZSTACK_COMPUTE_VM_10134, "ip address [%s] is not in ip range [startIp %s, endIp %s]", - ip, rangeVO.getStartIp(), rangeVO.getEndIp())); - } - } - } + validateStaticIpCommon(vmNicVO, l3NetworkVO, ip, IPv6Constants.IPv6, + ORG_ZSTACK_COMPUTE_VM_10132, ORG_ZSTACK_COMPUTE_VM_10133); } private void validate(APISetVmStaticIpMsg msg) { @@ -631,23 +628,41 @@ private void validate(APISetVmStaticIpMsg msg) { throw new ApiMessageInterceptionException(argerr(ORG_ZSTACK_COMPUTE_VM_10135, "could not set ip address, due to no ip address is specified")); } } - List ipv4Ranges = Q.New(NormalIpRangeVO.class) - .eq(NormalIpRangeVO_.l3NetworkUuid, msg.getL3NetworkUuid()) - .eq(NormalIpRangeVO_.ipVersion, IPv6Constants.IPv4).list(); - List ipv6Ranges = Q.New(NormalIpRangeVO.class) - .eq(NormalIpRangeVO_.l3NetworkUuid, msg.getL3NetworkUuid()) - .eq(NormalIpRangeVO_.ipVersion, IPv6Constants.IPv6).list(); List vmNics = Q.New(VmNicVO.class).eq(VmNicVO_.vmInstanceUuid, msg.getVmInstanceUuid()).list(); boolean l3Found = false; + + // Normalize IP addresses (avoid redundant calls) + String normalizedIp = null; + String normalizedIp6 = null; + UsedIpVO existingIpv4 = null; + UsedIpVO existingIpv6 = null; + for (VmNicVO nic : vmNics) { - l3Found = true; + if (msg.getL3NetworkUuid().equals(nic.getL3NetworkUuid())) { + l3Found = true; + } + + // Extract UsedIpVO records matching the same L3 and IP version from the NIC's existing IPs + for (UsedIpVO usedIp : nic.getUsedIps()) { + if (!msg.getL3NetworkUuid().equals(usedIp.getL3NetworkUuid())) { + continue; + } + + if (usedIp.getIpVersion() != null && usedIp.getIpVersion() == IPv6Constants.IPv4) { + existingIpv4 = usedIp; + } else if (usedIp.getIpVersion() != null && usedIp.getIpVersion() == IPv6Constants.IPv6) { + existingIpv6 = usedIp; + } + } if (msg.getIp() != null) { String ip = IPv6NetworkUtils.ipv6TagValueToAddress(msg.getIp()); if (NetworkUtils.isIpv4Address(ip)) { validateStaticIPv4(nic, l3NetworkVO, ip); + normalizedIp = ip; } else if (IPv6NetworkUtils.isIpv6Address(ip)) { validateStaticIPv6(nic, l3NetworkVO, ip); msg.setIp(ip); + normalizedIp6 = ip; } else { throw new ApiMessageInterceptionException(argerr(ORG_ZSTACK_COMPUTE_VM_10136, "static ip [%s] format error", msg.getIp())); } @@ -656,52 +671,52 @@ private void validate(APISetVmStaticIpMsg msg) { String ip6 = IPv6NetworkUtils.ipv6TagValueToAddress(msg.getIp6()); validateStaticIPv6(nic, l3NetworkVO, ip6); msg.setIp6(ip6); + normalizedIp6 = ip6; } } - if (msg.getIp() != null && !l3NetworkVO.enableIpAddressAllocation()) { - l3Found = true; - if (msg.getNetmask() == null) { - if (ipv4Ranges.isEmpty()) { - throw new ApiMessageInterceptionException(argerr(ORG_ZSTACK_COMPUTE_VM_10137, "ipv4 address need a netmask")); - } else { - msg.setNetmask(ipv4Ranges.get(0).getNetmask()); - } - } - if (msg.getGateway() == null) { - if (ipv4Ranges.isEmpty()) { - msg.setGateway(""); - } else { - msg.setGateway(ipv4Ranges.get(0).getGateway()); - } - } - if (Q.New(UsedIpVO.class).eq(UsedIpVO_.ip, msg.getIp()).eq(UsedIpVO_.l3NetworkUuid, msg.getL3NetworkUuid()).isExists()) { - throw new ApiMessageInterceptionException(argerr(ORG_ZSTACK_COMPUTE_VM_10138, "ip address [%s] already set to vmNic", msg.getIp())); - } - } - if (msg.getIp6() != null && !l3NetworkVO.enableIpAddressAllocation()) { - l3Found = true; - if (msg.getIpv6Prefix() == null) { - if (ipv6Ranges.isEmpty()) { - throw new ApiMessageInterceptionException(argerr(ORG_ZSTACK_COMPUTE_VM_10139, "ipv6 address need a prefix")); - } else { - msg.setIpv6Prefix(ipv6Ranges.get(0).getPrefixLen().toString()); - } - } - if (msg.getIpv6Gateway() == null) { - if (ipv6Ranges.isEmpty()) { - msg.setIpv6Gateway(""); - } else { - msg.setIpv6Gateway(ipv6Ranges.get(0).getGateway()); - } - } - if (Q.New(UsedIpVO.class).eq(UsedIpVO_.ip, msg.getIp6()).eq(UsedIpVO_.l3NetworkUuid, msg.getL3NetworkUuid()).isExists()) { - throw new ApiMessageInterceptionException(argerr(ORG_ZSTACK_COMPUTE_VM_10140, "ip address [%s] already set to vmNic", msg.getIp6())); - } - } + if (!l3Found) { throw new ApiMessageInterceptionException(argerr(ORG_ZSTACK_COMPUTE_VM_10141, "the VM[uuid:%s] has no nic on the L3 network[uuid:%s]", msg.getVmInstanceUuid(), - msg.getL3NetworkUuid())); + msg.getL3NetworkUuid())); } + + // Get the VM's default L3 network UUID for gateway enforcement + String defaultL3NetworkUuid = Q.New(VmInstanceVO.class) + .select(VmInstanceVO_.defaultL3NetworkUuid) + .eq(VmInstanceVO_.uuid, msg.getVmInstanceUuid()) + .findValue(); + boolean isDefaultNic = msg.getL3NetworkUuid().equals(defaultL3NetworkUuid); + boolean isOnlyNic = vmNics.size() == 1; + + StaticIpOperator staticIpOp = new StaticIpOperator(); + StaticIpOperator.NicRoleContext nicRole = new StaticIpOperator.NicRoleContext(isDefaultNic, isOnlyNic); + StaticIpOperator.ExistingIpContext existingIpCtx = new StaticIpOperator.ExistingIpContext(); + existingIpCtx.putIpv4(msg.getL3NetworkUuid(), existingIpv4); + existingIpCtx.putIpv6(msg.getL3NetworkUuid(), existingIpv6); + + // Fill parameters and check IP occupation using unified resolve + if (normalizedIp != null) { + List ipv4Ranges = Q.New(NormalIpRangeVO.class) + .eq(NormalIpRangeVO_.l3NetworkUuid, msg.getL3NetworkUuid()) + .eq(NormalIpRangeVO_.ipVersion, IPv6Constants.IPv4).list(); + String[] ipv4Result = staticIpOp.resolveIpv4NetmaskAndGateway(normalizedIp, msg.getNetmask(), msg.getGateway(), + ipv4Ranges, nicRole, existingIpv4); + msg.setNetmask(ipv4Result[0]); + msg.setGateway(ipv4Result[1]); + checkIpOccupied(normalizedIp, msg.getL3NetworkUuid()); + } + if (normalizedIp6 != null) { + List ipv6Ranges = Q.New(NormalIpRangeVO.class) + .eq(NormalIpRangeVO_.l3NetworkUuid, msg.getL3NetworkUuid()) + .eq(NormalIpRangeVO_.ipVersion, IPv6Constants.IPv6).list(); + String[] ipv6Result = staticIpOp.resolveIpv6PrefixAndGateway(normalizedIp6, msg.getIpv6Prefix(), msg.getIpv6Gateway(), + ipv6Ranges, nicRole, existingIpv6); + msg.setIpv6Prefix(ipv6Result[0]); + msg.setIpv6Gateway(ipv6Result[1]); + checkIpOccupied(normalizedIp6, msg.getL3NetworkUuid()); + } + + validateDnsAddresses(msg.getDnsAddresses()); } private void validate(APIDeleteVmStaticIpMsg msg) { @@ -825,26 +840,6 @@ private void validate(APICreateVmNicMsg msg) { } if (msg.getIp() != null) { - SimpleQuery iprq = dbf.createQuery(NormalIpRangeVO.class); - iprq.add(NormalIpRangeVO_.l3NetworkUuid, Op.EQ, msg.getL3NetworkUuid()); - List iprs = iprq.list(); - - boolean found = false; - for (NormalIpRangeVO ipr : iprs) { - if (NetworkUtils.isInRange(msg.getIp(), ipr.getStartIp(), ipr.getEndIp())) { - found = true; - break; - } - } - - if (!l3VO.enableIpAddressAllocation()) { - found = true; - } - - if (!found) { - throw new ApiMessageInterceptionException(argerr(ORG_ZSTACK_COMPUTE_VM_10154, "the static IP[%s] is not in any IP range of the L3 network[uuid:%s]", msg.getIp(), msg.getL3NetworkUuid())); - } - SimpleQuery uq = dbf.createQuery(UsedIpVO.class); uq.add(UsedIpVO_.l3NetworkUuid, Op.EQ, msg.getL3NetworkUuid()); uq.add(UsedIpVO_.ip, Op.EQ, msg.getIp()); @@ -936,29 +931,6 @@ private void validate(APIAttachL3NetworkToVmMsg msg) { .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue))); if (msg.getStaticIp() != null) { staticIps.computeIfAbsent(msg.getL3NetworkUuid(), k -> new ArrayList<>()).add(msg.getStaticIp()); - SimpleQuery iprq = dbf.createQuery(NormalIpRangeVO.class); - iprq.add(NormalIpRangeVO_.l3NetworkUuid, Op.EQ, msg.getL3NetworkUuid()); - List iprs = iprq.list(); - - boolean found = false; - for (NormalIpRangeVO ipr : iprs) { - if (!ipr.getIpVersion().equals(NetworkUtils.getIpversion(msg.getStaticIp()))) { - continue; - } - - if (NetworkUtils.isInRange(msg.getStaticIp(), ipr.getStartIp(), ipr.getEndIp())) { - found = true; - break; - } - } - - if (!l3NetworkVO.enableIpAddressAllocation()) { - found = true; - } - - if (!found) { - throw new ApiMessageInterceptionException(argerr(ORG_ZSTACK_COMPUTE_VM_10164, "the static IP[%s] is not in any IP range of the L3 network[uuid:%s]", msg.getStaticIp(), msg.getL3NetworkUuid())); - } SimpleQuery uq = dbf.createQuery(UsedIpVO.class); uq.add(UsedIpVO_.l3NetworkUuid, Op.EQ, msg.getL3NetworkUuid()); @@ -975,33 +947,8 @@ private void validate(APIAttachL3NetworkToVmMsg msg) { String l3Uuid = e.getKey(); List ips = e.getValue(); - SimpleQuery iprq = dbf.createQuery(NormalIpRangeVO.class); - iprq.add(NormalIpRangeVO_.l3NetworkUuid, Op.EQ, l3Uuid); - List iprs = iprq.list(); - boolean found = false; for (String staticIp : ips) { - int ipVersion = IPv6Constants.IPv4; - if (IPv6NetworkUtils.isIpv6Address(staticIp)) { - ipVersion = IPv6Constants.IPv6; - } - for (NormalIpRangeVO ipr : iprs) { - if (ipVersion != ipr.getIpVersion()) { - continue; - } - if (NetworkUtils.isInRange(staticIp, ipr.getStartIp(), ipr.getEndIp())) { - found = true; - break; - } - } - - if (!l3NetworkVO.enableIpAddressAllocation()) { - found = true; - } - - if (!found) { - throw new ApiMessageInterceptionException(argerr(ORG_ZSTACK_COMPUTE_VM_10167, "the static IP[%s] is not in any IP range of the L3 network[uuid:%s]", staticIp, l3Uuid)); - } SimpleQuery uq = dbf.createQuery(UsedIpVO.class); uq.add(UsedIpVO_.l3NetworkUuid, Op.EQ, msg.getL3NetworkUuid()); @@ -1424,7 +1371,7 @@ private void validate(NewVmInstanceMessage2 msg) { throw new ApiMessageInterceptionException(operr(ORG_ZSTACK_COMPUTE_VM_10206, "l3Network[uuid:%s] is Disabled, can not create vm on it", l3Uuid)); } if (system && (msg.getType() == null || VmInstanceConstant.USER_VM_TYPE.equals(msg.getType()))) { - throw new ApiMessageInterceptionException(operr(ORG_ZSTACK_COMPUTE_VM_10207, "l3Network[uuid:%s] is system network, can not create user vm on it", l3Uuid)); + throw new ApiMessageInterceptionException(argerr(ORG_ZSTACK_COMPUTE_VM_10207, "l3Network[uuid:%s] is system network, can not create user vm on it", l3Uuid)); } } } @@ -1589,4 +1536,4 @@ private void validate(APIFstrimVmMsg msg) { } msg.setHostUuid(t.get(1, String.class)); } -} +} \ No newline at end of file diff --git a/compute/src/main/java/org/zstack/compute/vm/VmInstanceBase.java b/compute/src/main/java/org/zstack/compute/vm/VmInstanceBase.java index d0ae6d3bcc9..58ff4e7b8d9 100755 --- a/compute/src/main/java/org/zstack/compute/vm/VmInstanceBase.java +++ b/compute/src/main/java/org/zstack/compute/vm/VmInstanceBase.java @@ -929,6 +929,7 @@ private void getStartingCandidateHosts(final NeedReplyMessage msg, final ReturnV amsg.setL3NetworkUuids(VmNicHelper.getL3Uuids(VmNicInventory.valueOf(self.getVmNics()))); amsg.setDryRun(true); amsg.setListAllHosts(true); + amsg.setAllowNoL3Networks(true); bus.send(amsg, new CloudBusCallBack(completion) { @Override @@ -1045,7 +1046,46 @@ public String getName() { }); } - private void changeVmIp(final String l3Uuid, final Map staticIpMap, final Completion completion) { + static class IpOverrideInfo { + private String netmask; + private String gateway; + private String ipv6Gateway; + private String ipv6Prefix; + + public String getNetmask() { + return netmask; + } + + public void setNetmask(String netmask) { + this.netmask = netmask; + } + + public String getGateway() { + return gateway; + } + + public void setGateway(String gateway) { + this.gateway = gateway; + } + + public String getIpv6Gateway() { + return ipv6Gateway; + } + + public void setIpv6Gateway(String ipv6Gateway) { + this.ipv6Gateway = ipv6Gateway; + } + + public String getIpv6Prefix() { + return ipv6Prefix; + } + + public void setIpv6Prefix(String ipv6Prefix) { + this.ipv6Prefix = ipv6Prefix; + } + } + + private void changeVmIp(final String l3Uuid, final Map staticIpMap, final IpOverrideInfo overrideInfo, final Completion completion) { final VmNicVO targetNic = CollectionUtils.find(self.getVmNics(), new Function() { @Override public VmNicVO call(VmNicVO arg) { @@ -1104,6 +1144,15 @@ public void run(final FlowTrigger trigger, Map data) { amsg.setL3NetworkUuid(l3Uuid); amsg.setRequiredIp(entry.getValue()); amsg.setIpVersion(entry.getKey()); + if (overrideInfo != null) { + if (entry.getKey() == IPv6Constants.IPv4) { + amsg.setNetmask(overrideInfo.getNetmask()); + amsg.setGateway(overrideInfo.getGateway()); + } else if (entry.getKey() == IPv6Constants.IPv6) { + amsg.setIpv6Gateway(overrideInfo.getIpv6Gateway()); + amsg.setIpv6Prefix(overrideInfo.getIpv6Prefix()); + } + } bus.makeTargetServiceIdByResourceUuid(amsg, L3NetworkConstant.SERVICE_ID, l3Uuid); bus.send(amsg, new CloudBusCallBack(trigger) { @Override @@ -3480,6 +3529,7 @@ private void handle(final APISetVmStaticIpMsg msg) { cmsg.setNetmask(msg.getNetmask()); cmsg.setIpv6Gateway(msg.getIpv6Gateway()); cmsg.setIpv6Prefix(msg.getIpv6Prefix()); + cmsg.setDnsAddresses(msg.getDnsAddresses()); bus.makeTargetServiceIdByResourceUuid(cmsg, VmInstanceConstant.SERVICE_ID, cmsg.getVmInstanceUuid()); bus.send(cmsg, new CloudBusCallBack(msg) { @Override @@ -3506,7 +3556,17 @@ public String getSyncSignature() { public void run(final SyncTaskChain chain) { L3NetworkVO l3NetworkVO = Q.New(L3NetworkVO.class).eq(L3NetworkVO_.uuid, msg.getL3NetworkUuid()).find(); - if (!l3NetworkVO.enableIpAddressAllocation()) { + + List staticIpList = new ArrayList<>(); + if (msg.getIp() != null) { + staticIpList.add(msg.getIp()); + } + if (msg.getIp6() != null) { + staticIpList.add(msg.getIp6()); + } + + if (!l3NetworkVO.enableIpAddressAllocation() + || allStaticIpsOutsideRange(msg.getL3NetworkUuid(), staticIpList)) { setNoIpamStaticIp(msg, new Completion(reply) { @Override public void success() { @@ -3651,6 +3711,11 @@ public void run(FlowTrigger trigger, Map data) { done(new FlowDoneHandler(completion) { @Override public void handle(Map data) { + // Set DNS addresses if provided + if (msg.getDnsAddresses() != null) { + new StaticIpOperator().setStaticDns(self.getUuid(), msg.getL3NetworkUuid(), msg.getDnsAddresses()); + } + completion.success(); } }); @@ -3679,7 +3744,13 @@ private void setIpamStaticIp(final SetVmStaticIpMsg msg, final Completion comple staticIpMap.put(IPv6Constants.IPv6, msg.getIp6()); } - changeVmIp(msg.getL3NetworkUuid(), staticIpMap, new Completion(msg, completion) { + IpOverrideInfo overrideInfo = new IpOverrideInfo(); + overrideInfo.setNetmask(msg.getNetmask()); + overrideInfo.setGateway(msg.getGateway()); + overrideInfo.setIpv6Gateway(msg.getIpv6Gateway()); + overrideInfo.setIpv6Prefix(msg.getIpv6Prefix()); + + changeVmIp(msg.getL3NetworkUuid(), staticIpMap, overrideInfo, new Completion(msg, completion) { @Override public void success() { if (msg.getIp() != null) { @@ -3689,6 +3760,10 @@ public void success() { new StaticIpOperator().setStaticIp(self.getUuid(), msg.getL3NetworkUuid(), msg.getIp6()); } new StaticIpOperator().setIpChange(self.getUuid(), msg.getL3NetworkUuid()); + // Set DNS addresses if provided + if (msg.getDnsAddresses() != null) { + new StaticIpOperator().setStaticDns(self.getUuid(), msg.getL3NetworkUuid(), msg.getDnsAddresses()); + } completion.success(); } @@ -5436,6 +5511,7 @@ public void handle(Map data) { private void removeStaticIp() { for (UsedIpInventory ip : nic.getUsedIps()) { new StaticIpOperator().deleteStaticIpByVmUuidAndL3Uuid(self.getUuid(), ip.getL3NetworkUuid()); + new StaticIpOperator().deleteStaticDnsByVmUuidAndL3Uuid(self.getUuid(), ip.getL3NetworkUuid()); } } @@ -6194,6 +6270,7 @@ private void handle(ChangeVmNicNetworkMsg msg) { public void success(VmNicInventory returnValue) { String originalL3Uuid = nic.getL3NetworkUuid(); new StaticIpOperator().deleteStaticIpByVmUuidAndL3Uuid(self.getUuid(), originalL3Uuid); + new StaticIpOperator().deleteStaticDnsByVmUuidAndL3Uuid(self.getUuid(), originalL3Uuid); reply.setInventory(returnValue); bus.reply(msg, reply); } @@ -6224,6 +6301,7 @@ private void handle(APIChangeVmNicNetworkMsg msg) { cmsg.setVmInstanceUuid(msg.getVmInstanceUuid()); cmsg.setRequiredIpMap(msg.getRequiredIpMap()); cmsg.setSystemTags(msg.getSystemTags()); + cmsg.setDnsAddresses(msg.getDnsAddresses()); bus.makeTargetServiceIdByResourceUuid(cmsg, VmInstanceConstant.SERVICE_ID, cmsg.getVmInstanceUuid()); bus.send(cmsg, new CloudBusCallBack(msg) { @Override @@ -6239,6 +6317,20 @@ public void run(MessageReply reply) { }); } + private boolean allStaticIpsOutsideRange(String l3Uuid, List ips) { + if (ips == null || ips.isEmpty()) { + return false; + } + + for (String ip : ips) { + if (new StaticIpOperator().getIpRangeUuid(l3Uuid, ip) != null) { + return false; + } + } + + return true; + } + private void changeVmNicNetwork(ChangeVmNicNetworkMsg msg, VmNicInventory nic, L3NetworkInventory destL3, final ReturnValueCompletion completion) { thdf.chainSubmit(new ChainTask(completion) { @Override @@ -6251,6 +6343,7 @@ public String getSyncSignature() { public void run(final SyncTaskChain chain) { class SetStaticIp { private boolean isSet = false; + private boolean isDnsSet = false; Map> staticIpMap = null; void set() { @@ -6271,17 +6364,28 @@ void set() { isSet = true; } + void setDns() { + if (msg.getDnsAddresses() != null) { + new StaticIpOperator().setStaticDns(self.getUuid(), msg.getDestL3NetworkUuid(), msg.getDnsAddresses()); + isDnsSet = true; + } + } + void rollback() { if (isSet) { for (Map.Entry> e : staticIpMap.entrySet()) { new StaticIpOperator().deleteStaticIpByVmUuidAndL3Uuid(self.getUuid(), e.getKey()); } } + if (isDnsSet) { + new StaticIpOperator().deleteStaticDnsByVmUuidAndL3Uuid(self.getUuid(), msg.getDestL3NetworkUuid()); + } } } final SetStaticIp setStaticIp = new SetStaticIp(); setStaticIp.set(); + setStaticIp.setDns(); Defer.guard(new Runnable() { @Override public void run() { @@ -6315,11 +6419,23 @@ public void run(FlowTrigger trigger, Map data) { @Override public void run(FlowTrigger trigger, Map data) { - if (!destL3.enableIpAddressAllocation()) { + if (!destL3.enableIpAddressAllocation() + || allStaticIpsOutsideRange(destL3.getUuid(), + msg.getRequiredIpMap() != null ? msg.getRequiredIpMap().get(destL3.getUuid()) : null)) { trigger.next(); return; } - allocateIp(destL3, nic, new ReturnValueCompletion>(chain) { + IpOverrideInfo nicOverrideInfo = null; + Map nicNetworkInfo = new StaticIpOperator().getNicNetworkInfoBySystemTag(msg.getSystemTags()); + NicIpAddressInfo nicIpInfo = nicNetworkInfo.get(msg.getDestL3NetworkUuid()); + if (nicIpInfo != null) { + nicOverrideInfo = new IpOverrideInfo(); + nicOverrideInfo.setNetmask(nicIpInfo.ipv4Netmask); + nicOverrideInfo.setGateway(nicIpInfo.ipv4Gateway); + nicOverrideInfo.setIpv6Gateway(nicIpInfo.ipv6Gateway); + nicOverrideInfo.setIpv6Prefix(nicIpInfo.ipv6Prefix); + } + allocateIp(destL3, nic, nicOverrideInfo, new ReturnValueCompletion>(chain) { @Override public void success(List returnValue) { data.put(VmInstanceConstant.Params.VmAllocateNicFlow_ips.toString(), returnValue); @@ -6369,7 +6485,9 @@ public void rollback(FlowRollback trigger, Map data) { @Override public void run(FlowTrigger trigger, Map data) { - if (destL3.enableIpAddressAllocation()) { + if (destL3.enableIpAddressAllocation() + && !allStaticIpsOutsideRange(destL3.getUuid(), + msg.getRequiredIpMap() != null ? msg.getRequiredIpMap().get(destL3.getUuid()) : null)) { trigger.next(); return; } @@ -6444,7 +6562,9 @@ public void run(FlowTrigger trigger, Map data) { @Override public void run(FlowTrigger trigger, Map data) { - if (!destL3.enableIpAddressAllocation()) { + if (!destL3.enableIpAddressAllocation() + || allStaticIpsOutsideRange(destL3.getUuid(), + msg.getRequiredIpMap() != null ? msg.getRequiredIpMap().get(destL3.getUuid()) : null)) { trigger.next(); return; } @@ -6616,7 +6736,7 @@ public String getName() { }); } - private void allocateIp(L3NetworkInventory l3, VmNicInventory nic,final ReturnValueCompletion> completion) { + private void allocateIp(L3NetworkInventory l3, VmNicInventory nic, final IpOverrideInfo overrideInfo, final ReturnValueCompletion> completion) { L3NetworkInventory nw = l3; Map> vmStaticIps = new StaticIpOperator().getStaticIpbyVmUuid(getSelf().getUuid()); List ipVersions = nw.getIpVersions(); @@ -6640,6 +6760,15 @@ private void allocateIp(L3NetworkInventory l3, VmNicInventory nic,final ReturnVa } } msg.setIpVersion(ipversion); + if (overrideInfo != null) { + if (ipversion == IPv6Constants.IPv4) { + msg.setNetmask(overrideInfo.getNetmask()); + msg.setGateway(overrideInfo.getGateway()); + } else if (ipversion == IPv6Constants.IPv6) { + msg.setIpv6Gateway(overrideInfo.getIpv6Gateway()); + msg.setIpv6Prefix(overrideInfo.getIpv6Prefix()); + } + } bus.makeTargetServiceIdByResourceUuid(msg, L3NetworkConstant.SERVICE_ID, nw.getUuid()); msgs.add(msg); } diff --git a/compute/src/main/java/org/zstack/compute/vm/VmQuotaOperator.java b/compute/src/main/java/org/zstack/compute/vm/VmQuotaOperator.java index bb63bbc9991..b8aa803644a 100644 --- a/compute/src/main/java/org/zstack/compute/vm/VmQuotaOperator.java +++ b/compute/src/main/java/org/zstack/compute/vm/VmQuotaOperator.java @@ -9,6 +9,7 @@ import org.zstack.core.db.SimpleQuery; import org.zstack.core.errorcode.ErrorFacade; import org.zstack.header.apimediator.ApiMessageInterceptionException; +import org.zstack.header.errorcode.ErrorCode; import org.zstack.header.identity.APIChangeResourceOwnerMsg; import org.zstack.header.identity.AccountType; import org.zstack.header.identity.Quota; @@ -151,12 +152,27 @@ private void checkTotalVMQuota(String currentAccountUuid, String vmInstanceUuid, long totalVmNumQuota, long totalVmNum) { + ErrorCode error = checkTotalVMQuotaWithResult(currentAccountUuid, + resourceTargetOwnerAccountUuid, + vmInstanceUuid, + totalVmNumQuota, + totalVmNum); + if (error != null) { + throw new ApiMessageInterceptionException(error); + } + } + + private ErrorCode checkTotalVMQuotaWithResult(String currentAccountUuid, + String resourceTargetOwnerAccountUuid, + String vmInstanceUuid, + long totalVmNumQuota, + long totalVmNum) { if (Q.New(VmInstanceVO.class) .eq(VmInstanceVO_.uuid, vmInstanceUuid) .notNull(VmInstanceVO_.lastHostUuid) .isExists()) { // Dirty hack - VM with last host UUID means existing VM. - return; + return null; } QuotaUtil.QuotaCompareInfo quotaCompareInfo; @@ -167,18 +183,17 @@ private void checkTotalVMQuota(String currentAccountUuid, quotaCompareInfo.quotaValue = totalVmNumQuota; quotaCompareInfo.currentUsed = totalVmNum; quotaCompareInfo.request = 1; - new QuotaUtil().CheckQuota(quotaCompareInfo); + return new QuotaUtil().checkQuotaAndReturn(quotaCompareInfo); } @Transactional(readOnly = true) - public void checkVmInstanceQuota(String currentAccountUuid, - String resourceTargetOwnerAccountUuid, - String vmInstanceUuid, - Map pairs) { + public ErrorCode checkVmInstanceQuotaWithResult(String currentAccountUuid, + String resourceTargetOwnerAccountUuid, + String vmInstanceUuid, + Map pairs) { long vmNumQuota = pairs.get(VmQuotaConstant.VM_RUNNING_NUM).getValue(); VmQuotaUtil.VmQuota vmQuotaUsed = new VmQuotaUtil().getUsedVmCpuMemory(resourceTargetOwnerAccountUuid, null); - // { QuotaUtil.QuotaCompareInfo quotaCompareInfo; quotaCompareInfo = new QuotaUtil.QuotaCompareInfo(); @@ -188,22 +203,38 @@ public void checkVmInstanceQuota(String currentAccountUuid, quotaCompareInfo.quotaValue = vmNumQuota; quotaCompareInfo.currentUsed = vmQuotaUsed.runningVmNum; quotaCompareInfo.request = 1; - new QuotaUtil().CheckQuota(quotaCompareInfo); + ErrorCode error = new QuotaUtil().checkQuotaAndReturn(quotaCompareInfo); + if (error != null) { + return error; + } } - // - checkTotalVMQuota(currentAccountUuid, + + ErrorCode error = checkTotalVMQuotaWithResult(currentAccountUuid, resourceTargetOwnerAccountUuid, vmInstanceUuid, pairs.get(VmQuotaConstant.VM_TOTAL_NUM).getValue(), vmQuotaUsed.totalVmNum); - // + if (error != null) { + return error; + } + VmInstanceVO vm = dbf.getEntityManager().find(VmInstanceVO.class, vmInstanceUuid); - checkVmCupAndMemoryCapacity(currentAccountUuid, resourceTargetOwnerAccountUuid, vm.getCpuNum(), vm.getMemorySize(), pairs); + return checkVmCupAndMemoryCapacityWithResult(currentAccountUuid, resourceTargetOwnerAccountUuid, vm.getCpuNum(), vm.getMemorySize(), pairs); + } + + public void checkVmInstanceQuota(String currentAccountUuid, + String resourceTargetOwnerAccountUuid, + String vmInstanceUuid, + Map pairs) { + ErrorCode error = checkVmInstanceQuotaWithResult(currentAccountUuid, resourceTargetOwnerAccountUuid, vmInstanceUuid, pairs); + if (error != null) { + throw new ApiMessageInterceptionException(error); + } } @Transactional(readOnly = true) - public void checkVmCupAndMemoryCapacity(String currentAccountUuid, String resourceTargetOwnerAccountUuid, long cpu, long memory, Map pairs) { + public ErrorCode checkVmCupAndMemoryCapacityWithResult(String currentAccountUuid, String resourceTargetOwnerAccountUuid, long cpu, long memory, Map pairs) { VmQuotaUtil.VmQuota vmQuotaUsed = new VmQuotaUtil().getUsedVmCpuMemory(resourceTargetOwnerAccountUuid); long cpuNumQuota = pairs.get(VmQuotaConstant.VM_RUNNING_CPU_NUM).getValue(); long memoryQuota = pairs.get(VmQuotaConstant.VM_RUNNING_MEMORY_SIZE).getValue(); @@ -217,7 +248,10 @@ public void checkVmCupAndMemoryCapacity(String currentAccountUuid, String resour quotaCompareInfo.quotaValue = cpuNumQuota; quotaCompareInfo.currentUsed = vmQuotaUsed.runningVmCpuNum; quotaCompareInfo.request = cpu; - new QuotaUtil().CheckQuota(quotaCompareInfo); + ErrorCode error = new QuotaUtil().checkQuotaAndReturn(quotaCompareInfo); + if (error != null) { + return error; + } } { QuotaUtil.QuotaCompareInfo quotaCompareInfo; @@ -228,7 +262,14 @@ public void checkVmCupAndMemoryCapacity(String currentAccountUuid, String resour quotaCompareInfo.quotaValue = memoryQuota; quotaCompareInfo.currentUsed = vmQuotaUsed.runningVmMemorySize; quotaCompareInfo.request = memory; - new QuotaUtil().CheckQuota(quotaCompareInfo); + return new QuotaUtil().checkQuotaAndReturn(quotaCompareInfo); + } + } + + public void checkVmCupAndMemoryCapacity(String currentAccountUuid, String resourceTargetOwnerAccountUuid, long cpu, long memory, Map pairs) { + ErrorCode error = checkVmCupAndMemoryCapacityWithResult(currentAccountUuid, resourceTargetOwnerAccountUuid, cpu, memory, pairs); + if (error != null) { + throw new ApiMessageInterceptionException(error); } } diff --git a/compute/src/main/java/org/zstack/compute/vm/VmSystemTags.java b/compute/src/main/java/org/zstack/compute/vm/VmSystemTags.java index 713c64890ee..df0c9fdd9e8 100755 --- a/compute/src/main/java/org/zstack/compute/vm/VmSystemTags.java +++ b/compute/src/main/java/org/zstack/compute/vm/VmSystemTags.java @@ -311,6 +311,11 @@ public String desensitizeTag(SystemTag systemTag, String tag) { } } + // DNS servers for VM NIC, format: staticDns::{l3NetworkUuid}::{dns1,dns2,dns3} + public static String STATIC_DNS_L3_UUID_TOKEN = "l3NetworkUuid"; + public static String STATIC_DNS_TOKEN = "staticDns"; + public static PatternedSystemTag STATIC_DNS = new PatternedSystemTag(String.format("staticDns::{%s}::{%s}", STATIC_DNS_L3_UUID_TOKEN, STATIC_DNS_TOKEN), VmInstanceVO.class); + public static PatternedSystemTag VM_STATE_PAUSED_AFTER_MIGRATE = new PatternedSystemTag(("vmPausedAfterMigrate"), VmInstanceVO.class); public static PatternedSystemTag VM_MEMORY_ACCESS_MODE_SHARED = new PatternedSystemTag(("vmMemoryAccessModeShared"), VmInstanceVO.class); diff --git a/compute/src/main/java/org/zstack/compute/zone/AbstractZone.java b/compute/src/main/java/org/zstack/compute/zone/AbstractZone.java index dd5e7f1c2fc..e327cd7f367 100755 --- a/compute/src/main/java/org/zstack/compute/zone/AbstractZone.java +++ b/compute/src/main/java/org/zstack/compute/zone/AbstractZone.java @@ -8,7 +8,6 @@ import org.zstack.header.zone.ZoneStateEvent; abstract class AbstractZone implements Zone { - private static DatabaseFacade dbf = Platform.getComponentLoader().getComponent(DatabaseFacade.class); private final static StateMachine stateMachine; static { diff --git a/conf/db/upgrade/V5.5.12__schema.sql b/conf/db/upgrade/V5.5.12__schema.sql deleted file mode 100644 index f5eaa76eea9..00000000000 --- a/conf/db/upgrade/V5.5.12__schema.sql +++ /dev/null @@ -1,33 +0,0 @@ --- ZSTAC-75319: Add normalizedModelName column for GPU spec dedup -CALL ADD_COLUMN('GpuDeviceSpecVO', 'normalizedModelName', 'VARCHAR(255)', 1, NULL); -CALL CREATE_INDEX('GpuDeviceSpecVO', 'idx_gpu_spec_normalized_model', 'normalizedModelName'); - --- Add totalScore and endTime columns to ModelEvaluationTaskVO for ZQL sorting support --- Previously these values were only stored inside the opaque JSON TEXT field, --- making them invisible to ZQL ORDER BY queries. -CALL ADD_COLUMN('ModelEvaluationTaskVO', 'totalScore', 'DOUBLE', 1, NULL); -CALL ADD_COLUMN('ModelEvaluationTaskVO', 'endTime', 'DATETIME', 1, NULL); - --- Add indexes to support efficient sorting -CALL CREATE_INDEX('ModelEvaluationTaskVO', 'idx_ModelEvaluationTaskVO_totalScore', 'totalScore'); -CALL CREATE_INDEX('ModelEvaluationTaskVO', 'idx_ModelEvaluationTaskVO_endTime', 'endTime'); - --- Backfill totalScore from opaque JSON for existing completed tasks --- Uses Json_getKeyValue defined in beforeMigrate.sql for MySQL 5.5+ compatibility -UPDATE `zstack`.`ModelEvaluationTaskVO` -SET `totalScore` = CAST(Json_getKeyValue(`opaque`, 'total_score') AS DECIMAL(20,6)) -WHERE `opaque` IS NOT NULL - AND `totalScore` IS NULL - AND Json_getKeyValue(`opaque`, 'total_score') IS NOT NULL; - --- Backfill endTime from opaque JSON for existing completed/failed tasks --- end_time format from Python agent: "MMM dd, yyyy hh:mm:ss a" (e.g. "Jan 01, 2025 10:30:00 AM") -UPDATE `zstack`.`ModelEvaluationTaskVO` -SET `endTime` = STR_TO_DATE( - Json_getKeyValue(`opaque`, 'end_time'), - '%b %d, %Y %h:%i:%s %p' -) -WHERE `opaque` IS NOT NULL - AND `endTime` IS NULL - AND Json_getKeyValue(`opaque`, 'end_time') IS NOT NULL - AND Json_getKeyValue(`opaque`, 'end_time') != ''; diff --git a/conf/db/upgrade/V5.5.16__schema.sql b/conf/db/upgrade/V5.5.16__schema.sql new file mode 100644 index 00000000000..8bb7cbbf9d7 --- /dev/null +++ b/conf/db/upgrade/V5.5.16__schema.sql @@ -0,0 +1,367 @@ +CALL ADD_COLUMN('AccessKeyVO', 'type', 'varchar(32)', 0, 'User'); + +-- ZSTAC-75319: Add normalizedModelName column for GPU spec dedup +CALL ADD_COLUMN('GpuDeviceSpecVO', 'normalizedModelName', 'VARCHAR(255)', 1, NULL); +CALL CREATE_INDEX('GpuDeviceSpecVO', 'idx_gpu_spec_normalized_model', 'normalizedModelName'); + +-- Add totalScore and endTime columns to ModelEvaluationTaskVO for ZQL sorting support +-- Previously these values were only stored inside the opaque JSON TEXT field, +-- making them invisible to ZQL ORDER BY queries. +CALL ADD_COLUMN('ModelEvaluationTaskVO', 'totalScore', 'DOUBLE', 1, NULL); +CALL ADD_COLUMN('ModelEvaluationTaskVO', 'endTime', 'DATETIME', 1, NULL); + +-- Add indexes to support efficient sorting +CALL CREATE_INDEX('ModelEvaluationTaskVO', 'idx_ModelEvaluationTaskVO_totalScore', 'totalScore'); +CALL CREATE_INDEX('ModelEvaluationTaskVO', 'idx_ModelEvaluationTaskVO_endTime', 'endTime'); + +-- Backfill totalScore from opaque JSON for existing completed tasks +-- Uses Json_getKeyValue defined in beforeMigrate.sql for MySQL 5.5+ compatibility +UPDATE `zstack`.`ModelEvaluationTaskVO` +SET `totalScore` = CAST(Json_getKeyValue(`opaque`, 'total_score') AS DECIMAL(20,6)) +WHERE `opaque` IS NOT NULL + AND `totalScore` IS NULL + AND Json_getKeyValue(`opaque`, 'total_score') IS NOT NULL; + +-- Backfill endTime from opaque JSON for existing completed/failed tasks +-- end_time format from Python agent: "MMM dd, yyyy hh:mm:ss a" (e.g. "Jan 01, 2025 10:30:00 AM") +UPDATE `zstack`.`ModelEvaluationTaskVO` +SET `endTime` = STR_TO_DATE( + Json_getKeyValue(`opaque`, 'end_time'), + '%b %d, %Y %h:%i:%s %p' +) +WHERE `opaque` IS NOT NULL + AND `endTime` IS NULL + AND Json_getKeyValue(`opaque`, 'end_time') IS NOT NULL + AND Json_getKeyValue(`opaque`, 'end_time') != ''; + +-- Add ExternalServiceConfiguration table +CREATE TABLE IF NOT EXISTS `zstack`.`ExternalServiceConfigurationVO` ( + `uuid` varchar(32) NOT NULL UNIQUE, + `serviceType` varchar(32) NOT NULL, + `configuration` text DEFAULT NULL, + `description` varchar(2048) DEFAULT NULL, + `lastOpDate` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + `createDate` timestamp NOT NULL DEFAULT '0000-00-00 00:00:00', + PRIMARY KEY (`uuid`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8; + +UPDATE VolumeSnapshotVO AS sp, PrimaryStorageVO AS ps +SET sp.primaryStorageInstallPath = REPLACE(sp.primaryStorageInstallPath, '/dev/', 'sharedblock://') +WHERE sp.primaryStorageUuid = ps.uuid AND ps.type = 'SharedBlock' AND sp.volumeType = 'Memory' AND sp.primaryStorageInstallPath LIKE '/dev/%'; + +UPDATE `zstack`.`ActiveAlarmTemplateVO` +SET `metricName` = 'CPUUsedUtilization' +WHERE `uuid` = 'c9e6cdca107140bea62b4ca919ff9e88' + AND `metricName` = 'VRouterCPUAverageUsedUtilization'; + +UPDATE `zstack`.`AlarmVO` +SET `metricName` = 'CPUUsedUtilization' +WHERE `uuid` IN ( + SELECT `alarmUuid` FROM `zstack`.`ActiveAlarmVO` + WHERE `templateUuid` = 'c9e6cdca107140bea62b4ca919ff9e88' +) + AND `metricName` = 'VRouterCPUAverageUsedUtilization'; + +-- ZSTAC-80472: Resource notification webhook tables +CREATE TABLE IF NOT EXISTS `zstack`.`ResNotifySubscriptionVO` ( + `uuid` VARCHAR(32) NOT NULL UNIQUE, + `name` VARCHAR(255) DEFAULT NULL, + `description` VARCHAR(2048) DEFAULT NULL, + `resourceTypes` TEXT DEFAULT NULL, + `eventTypes` VARCHAR(256) DEFAULT NULL, + `type` VARCHAR(32) NOT NULL DEFAULT 'WEBHOOK', + `state` VARCHAR(32) NOT NULL DEFAULT 'Enabled', + `accountUuid` VARCHAR(32) DEFAULT NULL, + `lastOpDate` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + `createDate` TIMESTAMP NOT NULL DEFAULT '0000-00-00 00:00:00', + PRIMARY KEY (`uuid`), + INDEX `idx_ResNotifySubscriptionVO_accountUuid` (`accountUuid`), + INDEX `idx_ResNotifySubscriptionVO_type_state` (`type`, `state`), + CONSTRAINT `fkResNotifySubscriptionVOResourceVO` FOREIGN KEY (`uuid`) REFERENCES `ResourceVO` (`uuid`) ON DELETE CASCADE +) ENGINE=InnoDB DEFAULT CHARSET=utf8; + +CREATE TABLE IF NOT EXISTS `zstack`.`ResNotifyWebhookRefVO` ( + `uuid` VARCHAR(32) NOT NULL UNIQUE, + `webhookUrl` TEXT NOT NULL, + `secret` VARCHAR(256) DEFAULT NULL, + `customHeaders` TEXT, + PRIMARY KEY (`uuid`), + CONSTRAINT `fk_ResNotifyWebhookRefVO_ResNotifySubscriptionVO` + FOREIGN KEY (`uuid`) REFERENCES `ResNotifySubscriptionVO`(`uuid`) ON DELETE CASCADE +) ENGINE=InnoDB DEFAULT CHARSET=utf8; + +CREATE TABLE IF NOT EXISTS `zstack`.`BareMetal2DpuChassisVO` ( + `uuid` varchar(32) NOT NULL UNIQUE, + `config` TEXT DEFAULT NULL, + `hostUuid` varchar(32) DEFAULT NULL, + PRIMARY KEY (`uuid`), + CONSTRAINT `fkBareMetal2DpuChassisVOChassisVO` FOREIGN KEY (`uuid`) REFERENCES `BareMetal2ChassisVO` (`uuid`) ON DELETE CASCADE, + CONSTRAINT `fkBareMetal2DpuChassisVOHostEO` FOREIGN KEY (`hostUuid`) REFERENCES `HostEO` (`uuid`) ON DELETE SET NULL + ) ENGINE=InnoDB DEFAULT CHARSET=utf8; + +CREATE TABLE IF NOT EXISTS `zstack`.`BareMetal2DpuHostVO` ( + `uuid` varchar(32) NOT NULL UNIQUE, + `chassisUuid` VARCHAR(32) NOT NULL, + `vendorType` VARCHAR(255) NOT NULL, + `url` VARCHAR(255) NOT NULL, + PRIMARY KEY (`uuid`), + CONSTRAINT `fkBareMetal2DpuHostVOHostVO` FOREIGN KEY (`uuid`) REFERENCES `HostEO` (`uuid`) ON DELETE CASCADE, + CONSTRAINT `fkBareMetal2DpuHostVOChassisVO` FOREIGN KEY (`chassisUuid`) REFERENCES `BareMetal2DpuChassisVO` (`uuid`) ON DELETE CASCADE + ) ENGINE=InnoDB DEFAULT CHARSET=utf8; + +ALTER TABLE `zstack`.`BareMetal2InstanceVO` +DROP FOREIGN KEY `fkBareMetal2InstanceVOGatewayVO`, +DROP FOREIGN KEY `fkBareMetal2InstanceVOGatewayVO1`; + +ALTER TABLE `zstack`.`BareMetal2InstanceVO` + ADD CONSTRAINT `fkBareMetal2InstanceVOGatewayVO` + FOREIGN KEY (`gatewayUuid`) + REFERENCES `HostEO` (`uuid`) + ON DELETE SET NULL, + ADD CONSTRAINT `fkBareMetal2InstanceVOGatewayVO1` + FOREIGN KEY (`lastGatewayUuid`) + REFERENCES `HostEO` (`uuid`) + ON DELETE SET NULL; + +-- ZSTAC-68709: Add targetQueueKey for evaluation task queuing per service endpoint +CALL ADD_COLUMN('ModelEvaluationTaskVO', 'targetQueueKey', 'TEXT', 1, NULL); + +-- Add prefixLen column to UsedIpVO for IPv6 addresses outside IP range +CALL ADD_COLUMN('UsedIpVO', 'prefixLen', 'INT', 1, NULL); + +-- Backfill prefixLen from IpRangeVO for existing IPv6 UsedIpVO records +UPDATE `zstack`.`UsedIpVO` `u` +INNER JOIN `zstack`.`IpRangeVO` `r` ON `u`.`ipRangeUuid` = `r`.`uuid` +SET `u`.`prefixLen` = `r`.`prefixLen` +WHERE `u`.`ipVersion` = 6 + AND `u`.`ipRangeUuid` IS NOT NULL + AND `u`.`prefixLen` IS NULL; + +-- Modify ipRangeUuid foreign key constraint to SET NULL on delete (instead of CASCADE) +DROP PROCEDURE IF EXISTS ModifyUsedIpVOForeignKey; +DELIMITER $$ + +CREATE PROCEDURE ModifyUsedIpVOForeignKey() +BEGIN + DECLARE constraint_exists INT; + + -- Check if the constraint exists before dropping it + SELECT COUNT(*) + INTO constraint_exists + FROM `INFORMATION_SCHEMA`.`TABLE_CONSTRAINTS` + WHERE `TABLE_SCHEMA` = 'zstack' + AND `TABLE_NAME` = 'UsedIpVO' + AND `CONSTRAINT_NAME` = 'fkUsedIpVOIpRangeEO' + AND `CONSTRAINT_TYPE` = 'FOREIGN KEY'; + + IF constraint_exists > 0 THEN + ALTER TABLE `zstack`.`UsedIpVO` DROP FOREIGN KEY `fkUsedIpVOIpRangeEO`; + END IF; + + -- Re-check before adding so the migration stays idempotent + SELECT COUNT(*) + INTO constraint_exists + FROM `INFORMATION_SCHEMA`.`TABLE_CONSTRAINTS` + WHERE `TABLE_SCHEMA` = 'zstack' + AND `TABLE_NAME` = 'UsedIpVO' + AND `CONSTRAINT_NAME` = 'fkUsedIpVOIpRangeEO' + AND `CONSTRAINT_TYPE` = 'FOREIGN KEY'; + + IF constraint_exists = 0 THEN + ALTER TABLE `zstack`.`UsedIpVO` + ADD CONSTRAINT `fkUsedIpVOIpRangeEO` + FOREIGN KEY (`ipRangeUuid`) REFERENCES `zstack`.`IpRangeEO`(`uuid`) ON DELETE SET NULL; + END IF; +END $$ + +DELIMITER ; + +CALL ModifyUsedIpVOForeignKey(); +DROP PROCEDURE IF EXISTS ModifyUsedIpVOForeignKey; + +-- Add ActiveAlarmTemplate rows for OVN (SDN) VM instance metric alarms +INSERT IGNORE INTO `ActiveAlarmTemplateVO` (`uuid`,`alarmName`,`comparisonOperator`,`period`,`repeatInterval`,`namespace`,`metricName`,`threshold`,`lastOpDate`,`createDate`,`repeatCount`,`enableRecovery`,`emergencyLevel`,`labels`) VALUES ('f1a6c2d85e7b8c9d3e4f5a6b7c8d9e0f','OvnVmInstance-DiskAllUsedCapacityInPercent','GreaterThanOrEqualTo',300,1800,'ZStack/OvnVmInstance','DiskAllUsedCapacityInPercent',80,CURRENT_TIMESTAMP(),CURRENT_TIMESTAMP(),-1,0,'Important',NULL); +INSERT IGNORE INTO `ActiveAlarmTemplateVO` (`uuid`,`alarmName`,`comparisonOperator`,`period`,`repeatInterval`,`namespace`,`metricName`,`threshold`,`lastOpDate`,`createDate`,`repeatCount`,`enableRecovery`,`emergencyLevel`,`labels`) VALUES ('b3c8e4f07a9d0e1f5a6b7c8d9e0f1a2b','OvnVmInstance-MemoryUsedInPercent','GreaterThanOrEqualTo',300,1800,'ZStack/OvnVmInstance','MemoryUsedInPercent',80,CURRENT_TIMESTAMP(),CURRENT_TIMESTAMP(),-1,0,'Important',NULL); +INSERT IGNORE INTO `ActiveAlarmTemplateVO` (`uuid`,`alarmName`,`comparisonOperator`,`period`,`repeatInterval`,`namespace`,`metricName`,`threshold`,`lastOpDate`,`createDate`,`repeatCount`,`enableRecovery`,`emergencyLevel`,`labels`) VALUES ('d5e0a6b29c1f2a3b7c8d9e0f1a2b3c4d','OvnVmInstance-CPUAverageUsedUtilization','GreaterThanOrEqualTo',300,1800,'ZStack/OvnVmInstance','CPUAverageUsedUtilization',80,CURRENT_TIMESTAMP(),CURRENT_TIMESTAMP(),-1,0,'Important',NULL); + +-- ZSTAC-74908: Add resourceType to TagPatternVO to scope AI model tags away from VM pages +CALL ADD_COLUMN('TagPatternVO', 'resourceType', 'VARCHAR(128)', 1, NULL); + +-- PCI virtualization capability metadata + +CREATE TABLE IF NOT EXISTS `zstack`.`PciDeviceVirtCapabilityVO` ( + `id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT, + `pciDeviceUuid` VARCHAR(32) NOT NULL, + `capability` VARCHAR(32) NOT NULL, + `createDate` TIMESTAMP NOT NULL, + `lastOpDate` TIMESTAMP NOT NULL, + PRIMARY KEY (`id`), + UNIQUE KEY `uk_pci_device_virt_capability` (`pciDeviceUuid`, `capability`), + KEY `idx_pci_device_virt_capability_pci` (`pciDeviceUuid`), + CONSTRAINT `fk_pci_device_virt_capability_pci` + FOREIGN KEY (`pciDeviceUuid`) REFERENCES `zstack`.`PciDeviceVO`(`uuid`) ON DELETE CASCADE +) ENGINE=InnoDB DEFAULT CHARSET=utf8; + +CALL ADD_COLUMN('PciDeviceVO', 'virtState', 'varchar(32)', 1, NULL); + +UPDATE `zstack`.`PciDeviceVO` +SET `virtState` = + CASE + WHEN `virtStatus` IN ('SRIOV_VIRTUALIZABLE', 'VFIO_MDEV_VIRTUALIZABLE', 'TENSORFUSION_VIRTUALIZABLE') THEN 'VIRTUALIZABLE' + WHEN `virtStatus` IN ('SRIOV_VIRTUALIZED', 'VFIO_MDEV_VIRTUALIZED', 'VIRTUALIZED_BYPASS_ZSTACK', + 'HAMI_VIRTUALIZED', 'TENSORFUSION_VIRTUALIZED') THEN 'VIRTUALIZED' + WHEN `virtStatus` = 'SRIOV_VIRTUAL' THEN 'VIRTUAL' + ELSE 'UNVIRTUALIZABLE' + END +WHERE `virtState` IS NULL; + +INSERT IGNORE INTO `zstack`.`PciDeviceVirtCapabilityVO` + (`pciDeviceUuid`, `capability`, `createDate`, `lastOpDate`) +SELECT `uuid`, 'SRIOV', NOW(), NOW() +FROM `zstack`.`PciDeviceVO` +WHERE `virtStatus` IN ('SRIOV_VIRTUALIZABLE', 'SRIOV_VIRTUALIZED'); + +INSERT IGNORE INTO `zstack`.`PciDeviceVirtCapabilityVO` + (`pciDeviceUuid`, `capability`, `createDate`, `lastOpDate`) +SELECT `uuid`, 'VFIO_MDEV', NOW(), NOW() +FROM `zstack`.`PciDeviceVO` +WHERE `virtStatus` IN ('VFIO_MDEV_VIRTUALIZABLE', 'VFIO_MDEV_VIRTUALIZED', 'VIRTUALIZED_BYPASS_ZSTACK'); + +INSERT IGNORE INTO `zstack`.`PciDeviceVirtCapabilityVO` + (`pciDeviceUuid`, `capability`, `createDate`, `lastOpDate`) +SELECT `uuid`, 'TENSORFUSION', NOW(), NOW() +FROM `zstack`.`PciDeviceVO` +WHERE `virtStatus` IN ('TENSORFUSION_VIRTUALIZABLE', 'TENSORFUSION_VIRTUALIZED'); + +INSERT IGNORE INTO `zstack`.`PciDeviceVirtCapabilityVO` + (`pciDeviceUuid`, `capability`, `createDate`, `lastOpDate`) +SELECT `uuid`, 'HAMI', NOW(), NOW() +FROM `zstack`.`PciDeviceVO` +WHERE `virtStatus` = 'HAMI_VIRTUALIZED'; + +CALL ADD_COLUMN('PciDeviceVO', 'virtMode', 'varchar(32)', 1, NULL); + +UPDATE `zstack`.`PciDeviceVO` +SET `virtMode` = + CASE + WHEN `virtStatus` IN ('SRIOV_VIRTUALIZED') THEN 'SRIOV' + WHEN `virtStatus` = 'SRIOV_VIRTUAL' THEN 'SRIOV' + WHEN `virtStatus` IN ('VFIO_MDEV_VIRTUALIZED', 'VIRTUALIZED_BYPASS_ZSTACK') THEN 'VFIO_MDEV' + WHEN `virtStatus` = 'TENSORFUSION_VIRTUALIZED' THEN 'TENSORFUSION' + WHEN `virtStatus` = 'HAMI_VIRTUALIZED' THEN 'HAMI' + ELSE `virtMode` + END +WHERE `virtStatus` IN ( + 'SRIOV_VIRTUALIZED', 'SRIOV_VIRTUAL', + 'VFIO_MDEV_VIRTUALIZED', 'VIRTUALIZED_BYPASS_ZSTACK', + 'TENSORFUSION_VIRTUALIZED', 'HAMI_VIRTUALIZED' +); + +CALL ADD_COLUMN('GpuDeviceVO', 'mode', 'varchar(32)', 1, NULL); +CALL CREATE_INDEX('GpuDeviceVO', 'idx_gpu_device_mode', 'mode'); + +UPDATE `zstack`.`GpuDeviceVO` g +INNER JOIN `zstack`.`PciDeviceVO` p ON g.`uuid` = p.`uuid` +SET g.`mode` = CASE + WHEN p.`virtState` = 'VIRTUALIZED' AND p.`virtMode` = 'TENSORFUSION' THEN 'DGPU' + WHEN p.`virtState` = 'VIRTUALIZED' AND p.`virtMode` IN ('VFIO_MDEV', 'SRIOV') THEN 'VGPU' + ELSE 'PCI' +END; + +UPDATE `zstack`.`GpuDeviceVO` g +INNER JOIN `zstack`.`PciDeviceVO` p ON g.`uuid` = p.`uuid` +SET g.`allocateStatus` = CASE + WHEN p.`vmInstanceUuid` IS NOT NULL THEN 'Allocated' + WHEN p.`virtState` = 'VIRTUALIZED' AND p.`virtMode` IS NOT NULL THEN 'Unallocatable' + ELSE 'Unallocated' +END; + +-- dGPU (TensorFusion) support tables + +CREATE TABLE IF NOT EXISTS `zstack`.`DGpuProfileVO` ( + `uuid` VARCHAR(32) NOT NULL, + `gpuSpecUuid` VARCHAR(32) NOT NULL, + `memorySize` BIGINT UNSIGNED NOT NULL, + `shmemSize` BIGINT UNSIGNED NOT NULL DEFAULT 268435456, + `createDate` TIMESTAMP NOT NULL, + `lastOpDate` TIMESTAMP NOT NULL, + PRIMARY KEY (`uuid`), + UNIQUE KEY `uk_dgpu_profile` (`gpuSpecUuid`, `memorySize`), + CONSTRAINT `fk_dgpu_profile_spec` + FOREIGN KEY (`gpuSpecUuid`) REFERENCES `zstack`.`GpuDeviceSpecVO`(`uuid`) ON DELETE CASCADE +) ENGINE=InnoDB DEFAULT CHARSET=utf8; + +CREATE TABLE IF NOT EXISTS `zstack`.`DGpuDeviceVO` ( + `uuid` VARCHAR(32) NOT NULL, + `name` VARCHAR(255) NOT NULL, + `parentGpuUuid` VARCHAR(32) NOT NULL, + `gpuSpecUuid` VARCHAR(32) NOT NULL, + `hostUuid` VARCHAR(32) NOT NULL, + `vmInstanceUuid` VARCHAR(32) DEFAULT NULL, + `allocatedMemory` BIGINT UNSIGNED NOT NULL, + `shmemSize` BIGINT UNSIGNED NOT NULL DEFAULT 268435456, + `smPercentLimit` INT NOT NULL DEFAULT 0, + `protocol` VARCHAR(16) NOT NULL DEFAULT 'shmem', + `status` VARCHAR(32) NOT NULL, + `vendorId` VARCHAR(64) DEFAULT NULL, + `vendor` VARCHAR(255) DEFAULT NULL, + `createDate` TIMESTAMP NOT NULL, + `lastOpDate` TIMESTAMP NOT NULL, + PRIMARY KEY (`uuid`), + INDEX `idx_dgpu_device_parent` (`parentGpuUuid`), + INDEX `idx_dgpu_device_spec` (`gpuSpecUuid`), + INDEX `idx_dgpu_device_host` (`hostUuid`), + INDEX `idx_dgpu_device_vm` (`vmInstanceUuid`), + CONSTRAINT `fk_dgpu_device_parent` + FOREIGN KEY (`parentGpuUuid`) REFERENCES `zstack`.`PciDeviceVO`(`uuid`) ON DELETE CASCADE, + CONSTRAINT `fk_dgpu_device_spec` + FOREIGN KEY (`gpuSpecUuid`) REFERENCES `zstack`.`GpuDeviceSpecVO`(`uuid`) ON DELETE RESTRICT, + CONSTRAINT `fk_dgpu_device_host` + FOREIGN KEY (`hostUuid`) REFERENCES `zstack`.`HostEO`(`uuid`) ON DELETE CASCADE, + CONSTRAINT `fk_dgpu_device_vm` + FOREIGN KEY (`vmInstanceUuid`) REFERENCES `zstack`.`VmInstanceEO`(`uuid`) ON DELETE SET NULL +) ENGINE=InnoDB DEFAULT CHARSET=utf8; + +CREATE TABLE IF NOT EXISTS `zstack`.`VmInstanceDGpuStrategyVO` ( + `id` BIGINT NOT NULL AUTO_INCREMENT, + `vmInstanceUuid` VARCHAR(32) NOT NULL, + `gpuSpecUuid` VARCHAR(32) NOT NULL, + `memorySize` BIGINT UNSIGNED NOT NULL, + `shmemSize` BIGINT UNSIGNED NOT NULL DEFAULT 268435456, + `gpuDeviceUuid` VARCHAR(32) DEFAULT NULL, + `chooser` VARCHAR(16) NOT NULL, + `autoDetachOnStop` TINYINT(1) NOT NULL DEFAULT 1, + `createDate` TIMESTAMP NOT NULL, + `lastOpDate` TIMESTAMP NOT NULL, + PRIMARY KEY (`id`), + UNIQUE KEY `uk_vm_dgpu_strategy` (`vmInstanceUuid`), + INDEX `idx_vm_dgpu_strategy_spec` (`gpuSpecUuid`), + CONSTRAINT `fk_vm_dgpu_strategy_vm` + FOREIGN KEY (`vmInstanceUuid`) REFERENCES `zstack`.`VmInstanceEO`(`uuid`) ON DELETE CASCADE, + CONSTRAINT `fk_vm_dgpu_strategy_spec` + FOREIGN KEY (`gpuSpecUuid`) REFERENCES `zstack`.`GpuDeviceSpecVO`(`uuid`) ON DELETE CASCADE, + CONSTRAINT `fk_vm_dgpu_strategy_device` + FOREIGN KEY (`gpuDeviceUuid`) REFERENCES `zstack`.`PciDeviceVO`(`uuid`) ON DELETE SET NULL +) ENGINE=InnoDB DEFAULT CHARSET=utf8; + +-- ZSTAC-83157: VM model mount table (virtiofs model mount to user VMs) +CREATE TABLE IF NOT EXISTS `zstack`.`VmModelMountVO` ( + `uuid` VARCHAR(32) NOT NULL, + `vmInstanceUuid` VARCHAR(32) NOT NULL, + `modelUuid` VARCHAR(32) NOT NULL, + `modelName` VARCHAR(256) DEFAULT NULL, + `mountPath` VARCHAR(512) NOT NULL, + `sourcePath` VARCHAR(1024) NOT NULL, + `status` VARCHAR(32) NOT NULL, + `accountUuid` VARCHAR(32) DEFAULT NULL, + `createDate` TIMESTAMP NOT NULL DEFAULT '0000-00-00 00:00:00', + `lastOpDate` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + PRIMARY KEY (`uuid`), + UNIQUE KEY `uk_vm_mountpath` (`vmInstanceUuid`, `mountPath`(255)), + UNIQUE KEY `uk_vm_model` (`vmInstanceUuid`, `modelUuid`), + CONSTRAINT `fk_vm_model_mount_vm` + FOREIGN KEY (`vmInstanceUuid`) REFERENCES `zstack`.`VmInstanceEO`(`uuid`) ON DELETE CASCADE, + CONSTRAINT `fk_vm_model_mount_model` + FOREIGN KEY (`modelUuid`) REFERENCES `zstack`.`ModelVO`(`uuid`) ON DELETE CASCADE +) ENGINE=InnoDB DEFAULT CHARSET=utf8; + diff --git a/conf/db/upgrade/beforeMigrate.sql b/conf/db/upgrade/beforeMigrate.sql index d53d98d8646..939e74eaa6c 100755 --- a/conf/db/upgrade/beforeMigrate.sql +++ b/conf/db/upgrade/beforeMigrate.sql @@ -5,12 +5,12 @@ DELIMITER $$ DROP FUNCTION IF EXISTS `Json_getKeyValue` $$ CREATE FUNCTION `Json_getKeyValue`( - in_JsonArray VARCHAR(4096), + in_JsonArray MEDIUMTEXT, in_KeyName VARCHAR(64) -) RETURNS VARCHAR(4096) CHARSET utf8 +) RETURNS MEDIUMTEXT CHARSET utf8 BEGIN - DECLARE vs_return, vs_JsonArray, vs_JsonString, vs_Json, vs_KeyName VARCHAR(4096); + DECLARE vs_return, vs_JsonArray, vs_JsonString, vs_Json, vs_KeyName MEDIUMTEXT; DECLARE vi_pos1, vi_pos2 SMALLINT UNSIGNED; SET vs_JsonArray = TRIM(in_JsonArray); diff --git a/conf/i18n/globalErrorCodeMapping/global-error-de-DE.json b/conf/i18n/globalErrorCodeMapping/global-error-de-DE.json index 81ae4d88c46..1e24c52ea11 100644 --- a/conf/i18n/globalErrorCodeMapping/global-error-de-DE.json +++ b/conf/i18n/globalErrorCodeMapping/global-error-de-DE.json @@ -35,7 +35,7 @@ "ORG_ZSTACK_STORAGE_SNAPSHOT_10008": "Volume[uuid:%s] kann nicht auf Snapshot[uuid:%s] zurückgesetzt werden, das Volume der VM[uuid:%s] befindet sich nicht im Status \"Stopped\", aktueller Status ist %s", "ORG_ZSTACK_STORAGE_PRIMARY_10044": "Kein qualifizierter primärer Speicher gefunden; Fehler sind %s", "ORG_ZSTACK_NETWORK_SERVICE_NFVINSTGROUP_10000": "Aktualisierung des Gruppenstatus fehlgeschlagen: %s", - "ORG_ZSTACK_STORAGE_PRIMARY_10050": "primaryStorageUuid-Konflikt: Der durch das Compute-Angebot angegebene primäre Speicher ist %s, während der durch den Erstellungsparameter angegebene primäre Speicher %s ist.", + "ORG_ZSTACK_STORAGE_PRIMARY_10050": "primaryStorageUuid-Konflikt: Der durch das Instanzangebot angegebene primäre Speicher ist %s, während der durch den Erstellungsparameter angegebene primäre Speicher %s ist.", "ORG_ZSTACK_STORAGE_PRIMARY_10051": "primaryStorageUuid-Konflikt: Der durch das Root-Disk-Angebot angegebene primäre Speicher ist %s, während der durch den Erstellungsparameter angegebene primäre Speicher %s ist.", "ORG_ZSTACK_V2V_10008": "Dieselbe MAC-Adresse [%s] ist im Netzwerk[%s] nicht erlaubt", "ORG_ZSTACK_V2V_10009": "Doppelte MAC-Adresse [%s] im Netzwerk[%s]", diff --git a/conf/i18n/globalErrorCodeMapping/global-error-en_US.json b/conf/i18n/globalErrorCodeMapping/global-error-en_US.json index f45787d6344..af8d1b29ac6 100644 --- a/conf/i18n/globalErrorCodeMapping/global-error-en_US.json +++ b/conf/i18n/globalErrorCodeMapping/global-error-en_US.json @@ -35,7 +35,7 @@ "ORG_ZSTACK_STORAGE_SNAPSHOT_10008": "unable to reset volume[uuid:%s] to snapshot[uuid:%s], the vm[uuid:%s] volume is not in Stopped state, current state is %s", "ORG_ZSTACK_STORAGE_PRIMARY_10044": "cannot find any qualified primary storage; errors are %s", "ORG_ZSTACK_NETWORK_SERVICE_NFVINSTGROUP_10000": "Failed to update group status: %s", - "ORG_ZSTACK_STORAGE_PRIMARY_10050": "primaryStorageUuid Conflict: The primary storage specified by the compute offering is %s, while the primary storage specified in the creation parameter is %s.", + "ORG_ZSTACK_STORAGE_PRIMARY_10050": "primaryStorageUuid Conflict: The primary storage specified by the instance offering is %s, while the primary storage specified in the creation parameter is %s.", "ORG_ZSTACK_STORAGE_PRIMARY_10051": "primaryStorageUuid Conflict: The primary storage specified by the root disk offering is %s, while the primary storage specified in the creation parameter is %s.", "ORG_ZSTACK_V2V_10008": "Not allowed the same MAC address [%s] in network[%s]", "ORG_ZSTACK_V2V_10009": "Duplicate MAC address [%s] in network[%s]", @@ -4717,5 +4717,8 @@ "ORG_ZSTACK_ZWATCH_FUNCTION_10013": "Unknown arguments detected. Please ensure that all command-line parameters are valid and refer to the documentation for a list of accepted arguments.", "ORG_ZSTACK_VPCFIREWALL_10034": "could not add firewall rule[%d] as only TCP protocol supports TCP flags in this environment", "ORG_ZSTACK_NETWORK_SERVICE_VIRTUALROUTER_VYOS_10007": "unable to stop DHCP server on virtual router instance [uuid:%s] because %s", - "ORG_ZSTACK_VPCFIREWALL_10035": "could not add firewall rule[%d] because of a %s error" -} \ No newline at end of file + "ORG_ZSTACK_VPCFIREWALL_10035": "could not add firewall rule[%d] because of a %s error", + "ORG_ZSTACK_DGPU_10010": "Available License not found, please apply addon license for product dGPU.", + "ORG_ZSTACK_DGPU_10011": "Addon license for product dGPU has expired, please renew it.", + "ORG_ZSTACK_DGPU_10012": "Insufficient dGPU GPU number licensed. Your license permits %d GPU, there are %d GPU used, shared: %d, need: %d." +} diff --git a/conf/i18n/globalErrorCodeMapping/global-error-fr-FR.json b/conf/i18n/globalErrorCodeMapping/global-error-fr-FR.json index 6a6ece44837..8fb00fa5776 100644 --- a/conf/i18n/globalErrorCodeMapping/global-error-fr-FR.json +++ b/conf/i18n/globalErrorCodeMapping/global-error-fr-FR.json @@ -35,7 +35,7 @@ "ORG_ZSTACK_STORAGE_SNAPSHOT_10008": "impossible de réinitialiser le volume[uuid:%s] vers l'instantané[uuid:%s], le volume vm[uuid:%s] n'est pas dans l'état Arrêté, l'état actuel est %s", "ORG_ZSTACK_STORAGE_PRIMARY_10044": "impossible de trouver un stockage principal qualifié ; les erreurs sont %s", "ORG_ZSTACK_NETWORK_SERVICE_NFVINSTGROUP_10000": "Échec de la mise à jour du statut du groupe : %s", - "ORG_ZSTACK_STORAGE_PRIMARY_10050": "Conflit primaryStorageUuid : le stockage principal spécifié par l'offre de calcul est %s, tandis que le stockage principal spécifié dans le paramètre de création est %s.", + "ORG_ZSTACK_STORAGE_PRIMARY_10050": "Conflit primaryStorageUuid : le stockage principal spécifié par l'offre d'instance est %s, tandis que le stockage principal spécifié dans le paramètre de création est %s.", "ORG_ZSTACK_STORAGE_PRIMARY_10051": "Conflit primaryStorageUuid : le stockage principal spécifié par l'offre de disque racine est %s, tandis que le stockage principal spécifié dans le paramètre de création est %s.", "ORG_ZSTACK_V2V_10008": "L'adresse MAC [%s] identique n'est pas autorisée dans le réseau[%s]", "ORG_ZSTACK_V2V_10009": "Adresse MAC [%s] en double dans le réseau[%s]", diff --git a/conf/i18n/globalErrorCodeMapping/global-error-id-ID.json b/conf/i18n/globalErrorCodeMapping/global-error-id-ID.json index c2bf216dea4..7e41658f314 100644 --- a/conf/i18n/globalErrorCodeMapping/global-error-id-ID.json +++ b/conf/i18n/globalErrorCodeMapping/global-error-id-ID.json @@ -35,7 +35,7 @@ "ORG_ZSTACK_STORAGE_SNAPSHOT_10008": "tidak dapat mengatur ulang volume[uuid:%s] ke snapshot[uuid:%s], volume vm[uuid:%s] tidak dalam keadaan Berhenti, keadaan saat ini adalah %s", "ORG_ZSTACK_STORAGE_PRIMARY_10044": "tidak dapat menemukan primary storage yang memenuhi syarat; error adalah %s", "ORG_ZSTACK_NETWORK_SERVICE_NFVINSTGROUP_10000": "Gagal memperbarui status grup: %s", - "ORG_ZSTACK_STORAGE_PRIMARY_10050": "Konflik primaryStorageUuid: Primary storage yang ditentukan oleh compute offering adalah %s, sementara primary storage yang ditentukan dalam parameter pembuatan adalah %s", + "ORG_ZSTACK_STORAGE_PRIMARY_10050": "Konflik primaryStorageUuid: Primary storage yang ditentukan oleh instance offering adalah %s, sementara primary storage yang ditentukan dalam parameter pembuatan adalah %s", "ORG_ZSTACK_STORAGE_PRIMARY_10051": "Konflik primaryStorageUuid: Primary storage yang ditentukan oleh root disk offering adalah %s, sementara primary storage yang ditentukan dalam parameter pembuatan adalah %s", "ORG_ZSTACK_V2V_10008": "Alamat MAC [%s] yang sama tidak diperbolehkan dalam jaringan[%s]", "ORG_ZSTACK_V2V_10009": "Alamat MAC duplikat [%s] dalam jaringan[%s]", diff --git a/conf/i18n/globalErrorCodeMapping/global-error-ja-JP.json b/conf/i18n/globalErrorCodeMapping/global-error-ja-JP.json index 5bd506c86a6..bf73aa503ff 100644 --- a/conf/i18n/globalErrorCodeMapping/global-error-ja-JP.json +++ b/conf/i18n/globalErrorCodeMapping/global-error-ja-JP.json @@ -35,7 +35,7 @@ "ORG_ZSTACK_STORAGE_SNAPSHOT_10008": "ボリューム[uuid:%s]をスナップショット[uuid:%s]にリセットできません。vm[uuid:%s]のボリュームは停止状態ではありません。現在のステータスは%sです", "ORG_ZSTACK_STORAGE_PRIMARY_10044": "適切なプライマリストレージが見つかりません。エラー: %s", "ORG_ZSTACK_NETWORK_SERVICE_NFVINSTGROUP_10000": "グループのステータスの更新に失敗しました: %s", - "ORG_ZSTACK_STORAGE_PRIMARY_10050": "primaryStorageUuidの競合。コンピュートオファリングによって指定されたプライマリストレージは%sであり、作成パラメータによって指定されたプライマリストレージは%sです。", + "ORG_ZSTACK_STORAGE_PRIMARY_10050": "primaryStorageUuidの競合。インスタンスオファリングによって指定されたプライマリストレージは%sであり、作成パラメータによって指定されたプライマリストレージは%sです。", "ORG_ZSTACK_STORAGE_PRIMARY_10051": "primaryStorageUuidの競合。ルートディスクオファリングによって指定されたプライマリストレージは%sであり、作成パラメータによって指定されたプライマリストレージは%sです。", "ORG_ZSTACK_V2V_10008": "ネットワーク[%s]で同じMACアドレス[%s]は許可されていません", "ORG_ZSTACK_V2V_10009": "ネットワーク[%s]でMACアドレス[%s]が重複しています", diff --git a/conf/i18n/globalErrorCodeMapping/global-error-ko-KR.json b/conf/i18n/globalErrorCodeMapping/global-error-ko-KR.json index 5d58f6618b1..d9323871208 100644 --- a/conf/i18n/globalErrorCodeMapping/global-error-ko-KR.json +++ b/conf/i18n/globalErrorCodeMapping/global-error-ko-KR.json @@ -35,7 +35,7 @@ "ORG_ZSTACK_STORAGE_SNAPSHOT_10008": "볼륨[uuid:%s]을 스냅샷[uuid:%s]으로 재설정할 수 없습니다, vm[uuid:%s] 볼륨이 Stopped 상태가 아니며, 현재 상태는 %s입니다", "ORG_ZSTACK_STORAGE_PRIMARY_10044": "적합한 기본 스토리지를 찾을 수 없습니다; 오류: %s", "ORG_ZSTACK_NETWORK_SERVICE_NFVINSTGROUP_10000": "그룹 상태 업데이트 실패: %s", - "ORG_ZSTACK_STORAGE_PRIMARY_10050": "primaryStorageUuid 충돌: 컴퓨트 오퍼링에서 지정한 기본 스토리지가 %s이며, 생성 파라미터에서 지정한 기본 스토리지가 %s입니다", + "ORG_ZSTACK_STORAGE_PRIMARY_10050": "primaryStorageUuid 충돌: 인스턴스 오퍼링에서 지정한 기본 스토리지가 %s이며, 생성 파라미터에서 지정한 기본 스토리지가 %s입니다", "ORG_ZSTACK_STORAGE_PRIMARY_10051": "primaryStorageUuid 충돌: 루트 디스크 오퍼링에서 지정한 기본 스토리지가 %s이며, 생성 파라미터에서 지정한 기본 스토리지가 %s입니다", "ORG_ZSTACK_V2V_10008": "네트워크[%s]에서 동일한 MAC 주소 [%s]가 허용되지 않습니다", "ORG_ZSTACK_V2V_10009": "네트워크[%s]에서 중복된 MAC 주소 [%s]", diff --git a/conf/i18n/globalErrorCodeMapping/global-error-ru-RU.json b/conf/i18n/globalErrorCodeMapping/global-error-ru-RU.json index 51a125842af..55bde5ee0c5 100644 --- a/conf/i18n/globalErrorCodeMapping/global-error-ru-RU.json +++ b/conf/i18n/globalErrorCodeMapping/global-error-ru-RU.json @@ -35,7 +35,7 @@ "ORG_ZSTACK_STORAGE_SNAPSHOT_10008": "невозможно сбросить том[uuid:%s] к снимку[uuid:%s], том ВМ[uuid:%s] не находится в состоянии Stopped, текущее состояние: %s", "ORG_ZSTACK_STORAGE_PRIMARY_10044": "невозможно найти подходящее основное хранилище; ошибки: %s", "ORG_ZSTACK_NETWORK_SERVICE_NFVINSTGROUP_10000": "не удалось обновить статус группы: %s", - "ORG_ZSTACK_STORAGE_PRIMARY_10050": "конфликт primaryStorageUuid, основное хранилище, указанное в предложении вычислений: %s, а основное хранилище, указанное в параметре создания: %s", + "ORG_ZSTACK_STORAGE_PRIMARY_10050": "конфликт primaryStorageUuid, основное хранилище, указанное в предложении экземпляра: %s, а основное хранилище, указанное в параметре создания: %s", "ORG_ZSTACK_STORAGE_PRIMARY_10051": "конфликт primaryStorageUuid, основное хранилище, указанное в предложении корневого диска: %s, а основное хранилище, указанное в параметре создания: %s", "ORG_ZSTACK_V2V_10008": "Не допускается одинаковый MAC-адрес [%s] в сети[%s]", "ORG_ZSTACK_V2V_10009": "Дублирующийся MAC-адрес [%s] в сети[%s]", diff --git a/conf/i18n/globalErrorCodeMapping/global-error-th-TH.json b/conf/i18n/globalErrorCodeMapping/global-error-th-TH.json index d8b7abf66d5..3118556b6d7 100644 --- a/conf/i18n/globalErrorCodeMapping/global-error-th-TH.json +++ b/conf/i18n/globalErrorCodeMapping/global-error-th-TH.json @@ -35,7 +35,7 @@ "ORG_ZSTACK_STORAGE_SNAPSHOT_10008": "ไม่สามารถรีเซ็ต volume[uuid:%s] ไปยัง snapshot[uuid:%s], volume ของ vm[uuid:%s] ไม่อยู่ในสถานะ Stopped, สถานะปัจจุบันคือ %s", "ORG_ZSTACK_STORAGE_PRIMARY_10044": "ไม่พบ primary storage ที่มีคุณสมบัติเหมาะสม; ข้อผิดพลาดคือ %s", "ORG_ZSTACK_NETWORK_SERVICE_NFVINSTGROUP_10000": "ไม่สามารถอัปเดตสถานะกลุ่ม: %s", - "ORG_ZSTACK_STORAGE_PRIMARY_10050": "ความขัดแย้งของ primaryStorageUuid: primary storage ที่ระบุโดย compute offering คือ %s ในขณะที่ primary storage ที่ระบุในพารามิเตอร์การสร้างคือ %s", + "ORG_ZSTACK_STORAGE_PRIMARY_10050": "ความขัดแย้งของ primaryStorageUuid: primary storage ที่ระบุโดย instance offering คือ %s ในขณะที่ primary storage ที่ระบุในพารามิเตอร์การสร้างคือ %s", "ORG_ZSTACK_STORAGE_PRIMARY_10051": "ความขัดแย้งของ primaryStorageUuid: primary storage ที่ระบุโดย root disk offering คือ %s ในขณะที่ primary storage ที่ระบุในพารามิเตอร์การสร้างคือ %s", "ORG_ZSTACK_V2V_10008": "ไม่อนุญาตให้มี MAC address [%s] ซ้ำกันใน network[%s]", "ORG_ZSTACK_V2V_10009": "MAC address [%s] ซ้ำกันใน network[%s]", diff --git a/conf/i18n/globalErrorCodeMapping/global-error-zh_CN.json b/conf/i18n/globalErrorCodeMapping/global-error-zh_CN.json index 5235832dccf..826e57aa36c 100644 --- a/conf/i18n/globalErrorCodeMapping/global-error-zh_CN.json +++ b/conf/i18n/globalErrorCodeMapping/global-error-zh_CN.json @@ -29,7 +29,7 @@ "ORG_ZSTACK_STORAGE_PRIMARY_10047": "cidr[%s] 输入格式错误", "ORG_ZSTACK_LICENSE_COMPUTE_SERVER_10050": "ZSha2 显示存在另一个管理节点,但数据库中未找到该节点", "ORG_ZSTACK_STORAGE_PRIMARY_10046": "仅允许一个主要存储 CIDR 系统标签,但获得了 %d 个", - "ORG_ZSTACK_STORAGE_PRIMARY_10049": "集群Uuid冲突,实例提供中指定的集群是%s,而创建参数中指定的集群是%s", + "ORG_ZSTACK_STORAGE_PRIMARY_10049": "集群Uuid冲突,计算规格中指定的集群是%s,而创建参数中指定的集群是%s", "ORG_ZSTACK_LICENSE_COMPUTE_SERVER_10052": "未找到解注册的appId!", "ORG_ZSTACK_STORAGE_PRIMARY_10042": "请指定分配空间的目的", "ORG_ZSTACK_STORAGE_SNAPSHOT_10008": "无法将卷[uuid:%s]恢复至快照[uuid:%s],关联的虚拟机[uuid:%s]卷当前不在已停止状态,当前状态是%s", @@ -4717,5 +4717,21 @@ "ORG_ZSTACK_ZWATCH_FUNCTION_10013": "未知参数", "ORG_ZSTACK_VPCFIREWALL_10034": "无法添加防火墙规则[%d],因为只有TCP协议可以使用TCP标志", "ORG_ZSTACK_NETWORK_SERVICE_VIRTUALROUTER_VYOS_10007": "无法停止虚拟路由器VM[uuid:%s]上的DHCP服务器,因为%s", - "ORG_ZSTACK_VPCFIREWALL_10035": "无法添加防火墙规则[%d]因为%s" -} \ No newline at end of file + "ORG_ZSTACK_VPCFIREWALL_10035": "无法添加防火墙规则[%d]因为%s", + "ORG_ZSTACK_DGPU_10010": "未找到可用的 dGPU AddOn License,请为 dGPU 产品申请并上传对应授权。", + "ORG_ZSTACK_DGPU_10011": "dGPU 产品的 AddOn License 已过期,请及时续期。", + "ORG_ZSTACK_DGPU_10012": "dGPU 授权 GPU 数量不足。License 允许 %d 个 GPU,当前已使用 %d 个,其他节点共享使用 %d 个,本次还需要 %d 个。", + "ORG_ZSTACK_AI_10138": "虚拟机[uuid:%s]未找到", + "ORG_ZSTACK_AI_10139": "模型[uuid:%s]未找到", + "ORG_ZSTACK_AI_10140": "虚拟机「%s」(UUID: %s) 必须处于运行状态才能挂载模型,当前状态: %s", + "ORG_ZSTACK_AI_10141": "虚拟机「%s」(UUID: %s) 未运行在任何主机上", + "ORG_ZSTACK_AI_10142": "模型「%s」(账户: %s) 与虚拟机「%s」(账户: %s) 属于不同账户,无法挂载。\n请确保模型和虚拟机属于同一账户。", + "ORG_ZSTACK_AI_10143": "模型「%s」(UUID: %s) 关联的模型中心(UUID: %s) 未找到。\n请检查模型中心是否已被删除。", + "ORG_ZSTACK_AI_10144": "挂载失败:路径 %s 已被该虚拟机上的另一个挂载占用。\n冲突挂载模型:%s\n冲突挂载UUID:%s\n所属虚拟机:%s\n所属虚拟机UUID:%s\n请先卸载该路径上的已有挂载,或使用其他挂载路径。", + "ORG_ZSTACK_AI_10145": "挂载路径[%s]不允许,不能挂载到系统目录", + "ORG_ZSTACK_AI_10146": "模型安装路径[%s]无效,预期格式: file:///root/bentoml/models/", + "ORG_ZSTACK_AI_10147": "挂载记录[uuid:%s]未找到", + "ORG_ZSTACK_AI_10148": "挂载记录「%s」(UUID: %s) 未处于已挂载状态,当前状态: %s", + "ORG_ZSTACK_AI_10149": "挂载模型到虚拟机失败: %s", + "ORG_ZSTACK_AI_10150": "模型「%s」(UUID: %s) 已挂载到虚拟机「%s」(UUID: %s),路径: %s。\n每个模型在每个虚拟机上只能挂载一次。" +} diff --git a/conf/i18n/globalErrorCodeMapping/global-error-zh_TW.json b/conf/i18n/globalErrorCodeMapping/global-error-zh_TW.json index 40aa862f414..b696d46d059 100644 --- a/conf/i18n/globalErrorCodeMapping/global-error-zh_TW.json +++ b/conf/i18n/globalErrorCodeMapping/global-error-zh_TW.json @@ -29,7 +29,7 @@ "ORG_ZSTACK_STORAGE_PRIMARY_10047": "cidr[%s] 輸入格式錯誤誤", "ORG_ZSTACK_LICENSE_COMPUTE_SERVER_10050": "ZSha2 显示儲在另一個管理節點,但數据库中未找到該節點", "ORG_ZSTACK_STORAGE_PRIMARY_10046": "仅允許一個主要儲儲 CIDR 系統統標签,但獲得了 %d 個", - "ORG_ZSTACK_STORAGE_PRIMARY_10049": "叢叢Uuid冲突,实例提供中指定的叢叢是%s,而創建參數中指定的叢叢是%s", + "ORG_ZSTACK_STORAGE_PRIMARY_10049": "叢集Uuid衝突,計算規格中指定的叢集是%s,而創建參數中指定的叢集是%s", "ORG_ZSTACK_LICENSE_COMPUTE_SERVER_10052": "未找到解注册的appId!", "ORG_ZSTACK_STORAGE_PRIMARY_10042": "請指定分配空間的目的", "ORG_ZSTACK_STORAGE_SNAPSHOT_10008": "無法将卷[uuid:%s]恢复至快照[uuid:%s],關聯的虚拟機[uuid:%s]卷當前不在已停止状态,當前状态是%s", diff --git a/conf/persistence.xml b/conf/persistence.xml index 30c36210dd6..b66d6319ff7 100755 --- a/conf/persistence.xml +++ b/conf/persistence.xml @@ -223,5 +223,6 @@ org.zstack.header.network.l3.L3NetworkSequenceNumberVO org.zstack.network.hostNetworkInterface.PhysicalSwitchVO org.zstack.network.hostNetworkInterface.PhysicalSwitchPortVO + org.zstack.header.core.external.service.ExternalServiceConfigurationVO diff --git a/conf/serviceConfig/externalService.xml b/conf/serviceConfig/externalService.xml index cb2f6c05d86..ae96366aa8a 100644 --- a/conf/serviceConfig/externalService.xml +++ b/conf/serviceConfig/externalService.xml @@ -9,4 +9,21 @@ org.zstack.header.core.external.service.APIReloadExternalServiceMsg + + + org.zstack.header.core.external.service.APIAddExternalServiceConfigurationMsg + + + + org.zstack.header.core.external.service.APIQueryExternalServiceConfigurationMsg + query + + + + org.zstack.header.core.external.service.APIUpdateExternalServiceConfigurationMsg + + + + org.zstack.header.core.external.service.APIDeleteExternalServiceConfigurationMsg + diff --git a/conf/springConfigXml/DatabaseFacade.xml b/conf/springConfigXml/DatabaseFacade.xml index 97b03278a02..d7afee98688 100755 --- a/conf/springConfigXml/DatabaseFacade.xml +++ b/conf/springConfigXml/DatabaseFacade.xml @@ -52,6 +52,7 @@ + sync - true + true 50 diff --git a/conf/springConfigXml/VolumeManager.xml b/conf/springConfigXml/VolumeManager.xml index 977134a8873..55a7fbad61c 100755 --- a/conf/springConfigXml/VolumeManager.xml +++ b/conf/springConfigXml/VolumeManager.xml @@ -49,7 +49,7 @@ - + diff --git a/conf/tools/install.sh b/conf/tools/install.sh index ca15a67650f..8a759869850 100755 --- a/conf/tools/install.sh +++ b/conf/tools/install.sh @@ -39,7 +39,9 @@ ensure_python3_venv() { if [ -d "$venv_path" ] && [ -x "$venv_path/bin/python3.11" ]; then return 0 fi - rm -rf "$venv_path" && python3.11 -m venv "$venv_path" || exit 1 + # retry once: rm -rf may fail if zstack_service_exporter is regenerating .pyc concurrently + rm -rf "$venv_path" || rm -rf "$venv_path" || exit 1 + python3.11 -m venv "$venv_path" || exit 1 } @@ -94,13 +96,8 @@ elif [ $tool = 'zstack-ctl' ]; then elif [ $tool = 'zstack-sys' ]; then SYS_VIRENV_PATH=/var/lib/zstack/virtualenv/zstacksys ensure_python3_venv "$SYS_VIRENV_PATH" - RE_INSTALL=false - . $SYS_VIRENV_PATH/bin/activate - if ! ansible --version | grep -q 'core 2.16.14'; then - deactivate - RE_INSTALL=true - fi - if $RE_INSTALL; then + # RE_INSTALL + if [ ! -x "$SYS_VIRENV_PATH/bin/ansible" ] || ! "$SYS_VIRENV_PATH/bin/ansible" --version 2>/dev/null | grep -q 'core 2.16.14'; then rm -rf $SYS_VIRENV_PATH && python3.11 -m venv $SYS_VIRENV_PATH || exit 1 . $SYS_VIRENV_PATH/bin/activate cd $cwd diff --git a/core/src/main/java/org/zstack/core/Platform.java b/core/src/main/java/org/zstack/core/Platform.java index ce5bada0d6c..78b184d12e7 100755 --- a/core/src/main/java/org/zstack/core/Platform.java +++ b/core/src/main/java/org/zstack/core/Platform.java @@ -17,6 +17,7 @@ import org.zstack.core.db.DatabaseGlobalProperty; import org.zstack.core.encrypt.EncryptRSA; import org.zstack.core.errorcode.ErrorFacade; +import org.zstack.core.errorcode.GlobalErrorCodeI18nService; import org.zstack.core.propertyvalidator.ValidatorTool; import org.zstack.core.search.SearchGlobalProperty; import org.zstack.core.statemachine.StateMachine; @@ -988,6 +989,23 @@ public static ErrorCode err(String globalErrorCode, Enum errCode, ErrorCode caus .toArray(String[]::new)); } + // populate message at creation time with default locale; + // RestServer will override with client's Accept-Language if different + try { + ComponentLoader currentLoader = loader; + if (currentLoader != null) { + GlobalErrorCodeI18nService i18nService = currentLoader.getComponent(GlobalErrorCodeI18nService.class); + if (i18nService != null) { + i18nService.localizeErrorCode(result, org.zstack.core.errorcode.LocaleUtils.DEFAULT_LOCALE); + } + } + } catch (Exception e) { + // i18n service not initialized during early startup + } + if (result.getMessage() == null) { + result.setMessage(details != null ? details : result.getDescription()); + } + return result; } diff --git a/core/src/main/java/org/zstack/core/ansible/CallBackNetworkChecker.java b/core/src/main/java/org/zstack/core/ansible/CallBackNetworkChecker.java index d2661d5cab8..031f624218c 100644 --- a/core/src/main/java/org/zstack/core/ansible/CallBackNetworkChecker.java +++ b/core/src/main/java/org/zstack/core/ansible/CallBackNetworkChecker.java @@ -72,6 +72,8 @@ public ErrorCode stopAnsible() { return useNcatAndNmapToTestConnection(ssh); } catch (SshException e) { return operr(ORG_ZSTACK_CORE_ANSIBLE_10004, e.getMessage()); + } finally { + ssh.close(); } } diff --git a/core/src/main/java/org/zstack/core/db/DatabaseFacade.java b/core/src/main/java/org/zstack/core/db/DatabaseFacade.java index 155aca5162b..efb4e22b307 100755 --- a/core/src/main/java/org/zstack/core/db/DatabaseFacade.java +++ b/core/src/main/java/org/zstack/core/db/DatabaseFacade.java @@ -84,4 +84,6 @@ public interface DatabaseFacade { String getDbVersion(); void installEntityLifeCycleCallback(Class entityClass, EntityEvent evt, EntityLifeCycleCallback cb); + + void uninstallEntityLifeCycleCallback(Class entityClass, EntityEvent evt, EntityLifeCycleCallback cb); } diff --git a/core/src/main/java/org/zstack/core/db/DatabaseFacadeImpl.java b/core/src/main/java/org/zstack/core/db/DatabaseFacadeImpl.java index d0a92426313..5a7b0c34c31 100755 --- a/core/src/main/java/org/zstack/core/db/DatabaseFacadeImpl.java +++ b/core/src/main/java/org/zstack/core/db/DatabaseFacadeImpl.java @@ -1,5 +1,7 @@ package org.zstack.core.db; +import com.google.common.collect.Maps; +import java.util.concurrent.CopyOnWriteArrayList; import org.hibernate.exception.ConstraintViolationException; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.core.NestedExceptionUtils; @@ -66,7 +68,7 @@ class EntityInfo { Field eoSoftDeleteColumn; Class eoClass; Class voClass; - Map listeners = new HashMap(); + Map> listeners = Maps.newConcurrentMap(); EntityInfo(Class voClazz) { voClass = voClazz; @@ -82,12 +84,16 @@ class EntityInfo { EO at = (EO) voClazz.getAnnotation(EO.class); if (at != null) { eoClass = at.EOClazz(); - DebugUtils.Assert(eoClass != null, String.format("cannot find EO entity specified by VO entity[%s]", voClazz.getName())); + DebugUtils.Assert(eoClass != null, + String.format("cannot find EO entity specified by VO entity[%s]", voClazz.getName())); eoPrimaryKeyField = FieldUtils.getAnnotatedField(Id.class, eoClass); - DebugUtils.Assert(eoPrimaryKeyField != null, String.format("cannot find primary key field(@Id annotated) in EO entity[%s]", eoClass.getName())); + DebugUtils.Assert(eoPrimaryKeyField != null, String + .format("cannot find primary key field(@Id annotated) in EO entity[%s]", eoClass.getName())); eoPrimaryKeyField.setAccessible(true); eoSoftDeleteColumn = FieldUtils.getField(at.softDeletedColumn(), eoClass); - DebugUtils.Assert(eoSoftDeleteColumn != null, String.format("cannot find soft delete column[%s] in EO entity[%s]", at.softDeletedColumn(), eoClass.getName())); + DebugUtils.Assert(eoSoftDeleteColumn != null, + String.format("cannot find soft delete column[%s] in EO entity[%s]", at.softDeletedColumn(), + eoClass.getName())); eoSoftDeleteColumn.setAccessible(true); } @@ -108,7 +114,8 @@ private void buildSoftDeletionCascade() { for (final SoftDeletionCascade at : ats.value()) { final Class parent = at.parent(); if (!parent.isAnnotationPresent(Entity.class)) { - throw new CloudRuntimeException(String.format("class[%s] has annotation @SoftDeletionCascade but its parent class[%s] is not annotated by @Entity", + throw new CloudRuntimeException(String.format( + "class[%s] has annotation @SoftDeletionCascade but its parent class[%s] is not annotated by @Entity", voClass, parent)); } @@ -131,7 +138,8 @@ public List getEntityClassForSoftDeleteEntityExtension() { @Override @Transactional public void postSoftDelete(Collection entityIds, Class entityClass) { - String sql = String.format("delete from %s me where me.%s in (:ids)", voClass.getSimpleName(), at.joinColumn()); + String sql = String.format("delete from %s me where me.%s in (:ids)", voClass.getSimpleName(), + at.joinColumn()); Query q = getEntityManager().createQuery(sql); q.setParameter("ids", entityIds); q.executeUpdate(); @@ -148,7 +156,8 @@ private void buildInheritanceDeletionExtension() { final Class parent = voClass.getSuperclass(); if (!parent.isAnnotationPresent(Entity.class)) { - throw new CloudRuntimeException(String.format("class[%s] has annotation @PrimaryKeyJoinColumn but its parent class[%s] is not annotated by @Entity", + throw new CloudRuntimeException(String.format( + "class[%s] has annotation @PrimaryKeyJoinColumn but its parent class[%s] is not annotated by @Entity", voClass, parent)); } @@ -245,7 +254,8 @@ private void updateEO(Object entity, RuntimeException de) { } SQLIntegrityConstraintViolationException me = (SQLIntegrityConstraintViolationException) rootCause; - if (!(me.getErrorCode() == 1062 && "23000".equals(me.getSQLState()) && me.getMessage().contains("PRIMARY"))) { + if (!(me.getErrorCode() == 1062 && "23000".equals(me.getSQLState()) + && me.getMessage().contains("PRIMARY"))) { throw de; } @@ -253,9 +263,12 @@ private void updateEO(Object entity, RuntimeException de) { throw de; } - // at this point, the error is caused by a update tried on VO entity which has been soft deleted. This is mostly - // caused by a deletion cascade(e.g deleting host will cause vm running on it to be deleted, and deleting vm is trying to return capacity - // to host which has been soft deleted, because vm deletion is executed in async manner). In this case, we make the update to EO table + // at this point, the error is caused by a update tried on VO entity which has + // been soft deleted. This is mostly + // caused by a deletion cascade(e.g deleting host will cause vm running on it to + // be deleted, and deleting vm is trying to return capacity + // to host which has been soft deleted, because vm deletion is executed in async + // manner). In this case, we make the update to EO table Object idval = getEOPrimaryKeyValue(entity); Object eo = getEntityManager().find(eoClass, idval); @@ -360,8 +373,10 @@ private void hardDelete(Collection ids) { @Transactional private void nativeSqlDelete(Collection ids) { - // native sql can avoid JPA cascades a deletion to parent entity when deleting a child entity - String sql = String.format("delete from %s where %s in (:ids)", voClass.getSimpleName(), voPrimaryKeyField.getName()); + // native sql can avoid JPA cascades a deletion to parent entity when deleting a + // child entity + String sql = String.format("delete from %s where %s in (:ids)", voClass.getSimpleName(), + voPrimaryKeyField.getName()); Query q = getEntityManager().createNativeQuery(sql); q.setParameter("ids", ids); q.executeUpdate(); @@ -418,7 +433,8 @@ List listByPrimaryKeys(Collection ids, int offset, int length) { sql = String.format("select e from %s e", voClass.getSimpleName()); query = getEntityManager().createQuery(sql, voClass); } else { - sql = String.format("select e from %s e where e.%s in (:ids)", voClass.getSimpleName(), voPrimaryKeyField.getName()); + sql = String.format("select e from %s e where e.%s in (:ids)", voClass.getSimpleName(), + voPrimaryKeyField.getName()); query = getEntityManager().createQuery(sql, voClass); query.setParameter("ids", ids); } @@ -429,7 +445,8 @@ List listByPrimaryKeys(Collection ids, int offset, int length) { @Transactional(readOnly = true, propagation = Propagation.REQUIRES_NEW) boolean isExist(Object id) { - String sql = String.format("select count(*) from %s ref where ref.%s = :id", voClass.getSimpleName(), voPrimaryKeyField.getName()); + String sql = String.format("select count(*) from %s ref where ref.%s = :id", voClass.getSimpleName(), + voPrimaryKeyField.getName()); TypedQuery q = getEntityManager().createQuery(sql, Long.class); q.setParameter("id", id); q.setMaxResults(1); @@ -438,13 +455,25 @@ boolean isExist(Object id) { } void installLifeCycleCallback(EntityEvent evt, EntityLifeCycleCallback l) { - listeners.put(evt, l); + List cbs = listeners.computeIfAbsent(evt, k -> new CopyOnWriteArrayList<>()); + if (!cbs.contains(l)) { + cbs.add(l); + } + } + + void uninstallLifeCycleCallback(EntityEvent evt, EntityLifeCycleCallback l) { + List cbs = listeners.get(evt); + if (cbs != null) { + cbs.remove(l); + } } void fireLifeCycleEvent(EntityEvent evt, Object o) { - EntityLifeCycleCallback cb = listeners.get(evt); - if (cb != null) { - cb.entityLifeCycleEvent(evt, o); + List cbs = listeners.get(evt); + if (cbs != null) { + for (EntityLifeCycleCallback cb : cbs) { + cb.entityLifeCycleEvent(evt, o); + } } } } @@ -491,7 +520,8 @@ public CriteriaBuilder getCriteriaBuilder() { @Override public SimpleQuery createQuery(Class entityClass) { - assert entityClass.isAnnotationPresent(Entity.class) : entityClass.getName() + " is not annotated by JPA @Entity"; + assert entityClass.isAnnotationPresent(Entity.class) + : entityClass.getName() + " is not annotated by JPA @Entity"; return new SimpleQueryImpl(entityClass); } @@ -530,7 +560,6 @@ public void removeByPrimaryKeys(Collection priKeys, Class entityClazz) { getEntityInfo(entityClazz).removeByPrimaryKeys(priKeys); } - @Override public T updateAndRefresh(T entity) { return (T) getEntityInfo(entity.getClass()).updateAndRefresh(entity); @@ -636,7 +665,9 @@ public void entityForTranscationCallback(Operation op, Class... entityClass) sb.append(c.getName()).append(","); } - String err = String.format("entityForTranscationCallback is called but transcation is not active. Did you forget adding @Transactional to method??? [operation: %s, entity classes: %s]", op, sb.toString()); + String err = String.format( + "entityForTranscationCallback is called but transcation is not active. Did you forget adding @Transactional to method??? [operation: %s, entity classes: %s]", + op, sb.toString()); logger.warn(err); } } @@ -668,7 +699,8 @@ public long generateSequenceNumber(Class seqTable) { try { Field id = seqTable.getDeclaredField("id"); if (id == null) { - throw new CloudRuntimeException(String.format("sequence VO[%s] must have 'id' field", seqTable.getName())); + throw new CloudRuntimeException( + String.format("sequence VO[%s] must have 'id' field", seqTable.getName())); } Object vo = seqTable.newInstance(); vo = persistAndRefresh(vo); @@ -770,7 +802,7 @@ public void eoCleanup(Class VOClazz) { @Override @DeadlockAutoRestart public void eoCleanup(Class VOClazz, Object id) { - if(id == null) { + if (id == null) { throw new RuntimeException(String.format("Cleanup %s EO fail, id is null", VOClazz.getSimpleName())); } @@ -798,7 +830,7 @@ public boolean start() { } private void buildEntityInfo() { - BeanUtils.reflections.getTypesAnnotatedWith(Entity.class).forEach(clz-> { + BeanUtils.reflections.getTypesAnnotatedWith(Entity.class).forEach(clz -> { entityInfoMap.put(clz, new EntityInfo(clz)); }); } @@ -820,7 +852,8 @@ private void populateExtensions() { } } - for (SoftDeleteEntityByEOExtensionPoint ext : pluginRgty.getExtensionList(SoftDeleteEntityByEOExtensionPoint.class)) { + for (SoftDeleteEntityByEOExtensionPoint ext : pluginRgty + .getExtensionList(SoftDeleteEntityByEOExtensionPoint.class)) { for (Class eoClass : ext.getEOClassForSoftDeleteEntityExtension()) { List exts = softDeleteByEOExtensions.get(eoClass); if (exts == null) { @@ -873,6 +906,20 @@ public void installEntityLifeCycleCallback(Class clz, EntityEvent evt, EntityLif } } + @Override + public void uninstallEntityLifeCycleCallback(Class clz, EntityEvent evt, EntityLifeCycleCallback cb) { + if (clz != null) { + EntityInfo info = entityInfoMap.get(clz); + if (info != null) { + info.uninstallLifeCycleCallback(evt, cb); + } + } else { + for (EntityInfo info : entityInfoMap.values()) { + info.uninstallLifeCycleCallback(evt, cb); + } + } + } + @Override public boolean stop() { return true; @@ -881,7 +928,8 @@ public boolean stop() { void entityEvent(EntityEvent evt, Object entity) { EntityInfo info = entityInfoMap.get(entity.getClass()); if (info == null) { - logger.warn(String.format("cannot find EntityInfo for the class[%s], not entity events will be fired", entity.getClass())); + logger.warn(String.format("cannot find EntityInfo for the class[%s], not entity events will be fired", + entity.getClass())); return; } diff --git a/core/src/main/java/org/zstack/core/errorcode/GlobalErrorCodeI18nServiceImpl.java b/core/src/main/java/org/zstack/core/errorcode/GlobalErrorCodeI18nServiceImpl.java index f4def1fc071..b6820fc2d3b 100644 --- a/core/src/main/java/org/zstack/core/errorcode/GlobalErrorCodeI18nServiceImpl.java +++ b/core/src/main/java/org/zstack/core/errorcode/GlobalErrorCodeI18nServiceImpl.java @@ -56,7 +56,7 @@ private void loadAllJsonFiles() { String content = new String(Files.readAllBytes(file.toPath()), StandardCharsets.UTF_8); @SuppressWarnings("unchecked") Map messages = JSONObjectUtil.toObject(content, LinkedHashMap.class); - localeMessages.put(locale, messages); + localeMessages.put(locale, Collections.unmodifiableMap(messages)); logger.info(String.format("loaded %d i18n error messages for locale [%s]", messages.size(), locale)); } catch (Exception e) { @@ -125,26 +125,34 @@ private String formatTemplate(String template, String[] formatArgs) { @Override public void localizeErrorCode(ErrorCode error, String locale) { - if (error == null || locale == null) { + if (error == null) { return; } + String resolvedLocale = locale != null ? locale : LocaleUtils.DEFAULT_LOCALE; + if (error.getGlobalErrorCode() != null) { - String message = getLocalizedMessage(error.getGlobalErrorCode(), locale, error.getFormatArgs()); + String message = getLocalizedMessage(error.getGlobalErrorCode(), resolvedLocale, error.getFormatArgs()); if (message != null) { error.setMessage(message); } } + // guarantee: message is never null + if (error.getMessage() == null) { + String fallback = error.getDetails() != null ? error.getDetails() : error.getDescription(); + error.setMessage(fallback != null ? fallback : (error.getCode() != null ? error.getCode() : "")); + } + if (error.getCause() != null) { - localizeErrorCode(error.getCause(), locale); + localizeErrorCode(error.getCause(), resolvedLocale); } if (error instanceof ErrorCodeList) { List causes = ((ErrorCodeList) error).getCauses(); if (causes != null) { for (ErrorCode cause : causes) { - localizeErrorCode(cause, locale); + localizeErrorCode(cause, resolvedLocale); } } } diff --git a/core/src/main/java/org/zstack/core/errorcode/LocaleUtils.java b/core/src/main/java/org/zstack/core/errorcode/LocaleUtils.java index 8820eadbfbe..d97e9c0f0d6 100644 --- a/core/src/main/java/org/zstack/core/errorcode/LocaleUtils.java +++ b/core/src/main/java/org/zstack/core/errorcode/LocaleUtils.java @@ -30,6 +30,9 @@ public class LocaleUtils { * Parse Accept-Language header and return the best matching locale key * from the set of available locales. * + * Uses {@link Locale.LanguageRange#parse(String)} for RFC 7231 compliant + * parsing with proper q-value priority sorting. + * * @param acceptLanguage the Accept-Language header value * @param availableLocales the set of locale keys loaded from JSON files * @return the best matching locale key, or en_US as fallback @@ -39,18 +42,27 @@ public static String resolveLocale(String acceptLanguage, Set availableL return DEFAULT_LOCALE; } - List entries = parseAcceptLanguage(acceptLanguage); - for (LocaleEntry entry : entries) { - if (entry.quality <= 0) { + List ranges; + try { + ranges = Locale.LanguageRange.parse(acceptLanguage); + } catch (IllegalArgumentException e) { + logger.debug(String.format("failed to parse Accept-Language [%s]: %s", acceptLanguage, e.getMessage())); + return DEFAULT_LOCALE; + } + + // ranges are already sorted by q-value descending + for (Locale.LanguageRange range : ranges) { + if (range.getWeight() <= 0) { continue; } - String normalized = normalizeTag(entry.tag); + String tag = range.getRange(); + String normalized = normalizeTag(tag); if (availableLocales.contains(normalized)) { return normalized; } - String lang = entry.tag.split("[-_]")[0].toLowerCase(); + String lang = tag.split("[-_]")[0].toLowerCase(); String mapped = LANGUAGE_TO_LOCALE.get(lang); if (mapped != null && availableLocales.contains(mapped)) { return mapped; @@ -78,42 +90,4 @@ static String normalizeTag(String tag) { } return tag; } - - private static List parseAcceptLanguage(String header) { - List entries = new ArrayList<>(); - String[] parts = header.split(","); - for (String part : parts) { - part = part.trim(); - if (part.isEmpty()) { - continue; - } - String[] tagAndParams = part.split(";"); - String tag = tagAndParams[0].trim(); - double quality = 1.0; - for (int i = 1; i < tagAndParams.length; i++) { - String param = tagAndParams[i].trim(); - if (param.startsWith("q=")) { - try { - quality = Double.parseDouble(param.substring(2).trim()); - } catch (NumberFormatException e) { - logger.debug(String.format("failed to parse quality value [%s]: %s", param, e.getMessage())); - quality = 0; - } - } - } - entries.add(new LocaleEntry(tag, quality)); - } - entries.sort((a, b) -> Double.compare(b.quality, a.quality)); - return entries; - } - - private static class LocaleEntry { - final String tag; - final double quality; - - LocaleEntry(String tag, double quality) { - this.tag = tag; - this.quality = quality; - } - } } diff --git a/core/src/main/java/org/zstack/core/externalservice/ExternalService.java b/core/src/main/java/org/zstack/core/externalservice/ExternalService.java index 2b816f00337..01f29f90f1e 100755 --- a/core/src/main/java/org/zstack/core/externalservice/ExternalService.java +++ b/core/src/main/java/org/zstack/core/externalservice/ExternalService.java @@ -16,4 +16,10 @@ public interface ExternalService { ExternalServiceCapabilities getExternalServiceCapabilities(); void reload(); + + String getServiceType(); + + default void externalConfig(String serviceType) { + // no-op by default + }; } diff --git a/core/src/main/java/org/zstack/core/externalservice/ExternalServiceManagerImpl.java b/core/src/main/java/org/zstack/core/externalservice/ExternalServiceManagerImpl.java index 4f4ad548dad..4d39ffc4888 100755 --- a/core/src/main/java/org/zstack/core/externalservice/ExternalServiceManagerImpl.java +++ b/core/src/main/java/org/zstack/core/externalservice/ExternalServiceManagerImpl.java @@ -1,19 +1,36 @@ package org.zstack.core.externalservice; import org.springframework.beans.factory.annotation.Autowired; +import org.zstack.core.CoreGlobalProperty; +import org.zstack.core.GlobalProperty; +import org.zstack.core.Platform; +import org.zstack.core.asyncbatch.While; import org.zstack.core.cloudbus.CloudBus; +import org.zstack.core.cloudbus.CloudBusCallBack; +import org.zstack.core.db.DatabaseFacade; +import org.zstack.core.db.Q; +import org.zstack.core.thread.ChainTask; +import org.zstack.core.thread.SyncTaskChain; +import org.zstack.core.thread.ThreadFacade; +import org.zstack.core.workflow.SimpleFlowChain; import org.zstack.header.AbstractService; -import org.zstack.header.core.external.service.APIGetExternalServicesMsg; -import org.zstack.header.core.external.service.APIGetExternalServicesReply; -import org.zstack.header.core.external.service.APIReloadExternalServiceEvent; -import org.zstack.header.core.external.service.APIReloadExternalServiceMsg; -import org.zstack.header.core.external.service.ExternalServiceInventory; -import org.zstack.header.core.external.service.ExternalServiceStatus; +import org.zstack.header.core.Completion; +import org.zstack.header.core.ReturnValueCompletion; +import org.zstack.header.core.WhileDoneCompletion; +import org.zstack.header.core.external.service.*; +import org.zstack.header.core.workflow.*; +import org.zstack.header.errorcode.ErrorCode; +import org.zstack.header.errorcode.ErrorCodeList; import org.zstack.header.errorcode.OperationFailureException; +import org.zstack.header.managementnode.ManagementNodeVO; +import org.zstack.header.managementnode.ManagementNodeVO_; import org.zstack.header.message.APIMessage; import org.zstack.header.message.Message; +import org.zstack.header.message.MessageReply; import java.util.ArrayList; +import java.util.Collections; +import java.util.List; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; import java.util.function.Supplier; @@ -24,6 +41,10 @@ public class ExternalServiceManagerImpl extends AbstractService implements ExternalServiceManager { @Autowired public CloudBus bus; + @Autowired + private DatabaseFacade dbf; + @Autowired + private ThreadFacade thdf; private final Map services = new ConcurrentHashMap<>(); @@ -72,8 +93,8 @@ public boolean stop() { public void handleMessage(Message msg) { if (msg instanceof APIMessage) { handleApiMessage((APIMessage) msg); - } else { - bus.dealWithUnknownMessage(msg); + } else { + handleLocalMessage(msg); } } @@ -82,6 +103,20 @@ public void handleApiMessage(APIMessage msg) { handle((APIGetExternalServicesMsg) msg); } else if (msg instanceof APIReloadExternalServiceMsg) { handle((APIReloadExternalServiceMsg) msg); + } else if (msg instanceof APIAddExternalServiceConfigurationMsg){ + handle((APIAddExternalServiceConfigurationMsg) msg); + } else if (msg instanceof APIUpdateExternalServiceConfigurationMsg) { + handle((APIUpdateExternalServiceConfigurationMsg) msg); + } else if (msg instanceof APIDeleteExternalServiceConfigurationMsg) { + handle((APIDeleteExternalServiceConfigurationMsg) msg); + } else { + bus.dealWithUnknownMessage(msg); + } + } + + private void handleLocalMessage(Message msg) { + if (msg instanceof ApplyExternalServiceConfigurationMsg) { + handle((ApplyExternalServiceConfigurationMsg) msg); } else { bus.dealWithUnknownMessage(msg); } @@ -118,12 +153,287 @@ private void handle(APIGetExternalServicesMsg msg) { inv.setName(name); inv.setStatus(service.isAlive() ? ExternalServiceStatus.RUNNING.toString() : ExternalServiceStatus.STOPPED.toString()); inv.setCapabilities(service.getExternalServiceCapabilities()); + inv.setServiceType(service.getServiceType()); reply.getInventories().add(inv); }); bus.reply(msg, reply); } + private void handle(APIAddExternalServiceConfigurationMsg msg ){ + APIAddExternalServiceConfigurationEvent event = new APIAddExternalServiceConfigurationEvent(msg.getId()); + + thdf.chainSubmit(new ChainTask(msg) { + @Override + public void run(SyncTaskChain chain) { + createExternalServiceConfiguration(msg, event, new Completion(chain) { + @Override + public void success() { + bus.publish(event); + chain.next(); + } + + @Override + public void fail(ErrorCode errorCode) { + event.setError(errorCode); + bus.publish(event); + chain.next(); + } + }); + } + + @Override + public String getSyncSignature() { + return String.format("create-update-delete-external-service-configuration-%s", msg.getExternalServiceType()); + } + + @Override + public String getName() { + return String.format("create-external-service-configuration-type-%s", msg.getExternalServiceType()); + } + }); + + } + + private void createExternalServiceConfiguration(APIAddExternalServiceConfigurationMsg msg, APIAddExternalServiceConfigurationEvent evt, Completion completion) { + // create db record + ExternalServiceConfigurationVO configurationVO = new ExternalServiceConfigurationVO(); + configurationVO.setUuid(msg.getResourceUuid() != null ? msg.getResourceUuid() : Platform.getUuid()); + configurationVO.setServiceType(msg.getExternalServiceType()); + configurationVO.setConfiguration(msg.getConfiguration()); + configurationVO.setDescription(msg.getDescription()); + configurationVO = dbf.persistAndRefresh(configurationVO); + + ExternalServiceConfigurationInventory inv = ExternalServiceConfigurationInventory.valueOf(configurationVO); + + applyExternalServiceConfigurationToAllNodes(configurationVO.getServiceType(), new ReturnValueCompletion>(completion) { + @Override + public void success(List returnValue) { + evt.setInventory(inv); + completion.success(); + } + + @Override + public void fail(ErrorCode errorCode) { + completion.fail(errorCode); + } + }); + } + + private void handle(APIUpdateExternalServiceConfigurationMsg msg){ + APIUpdateExternalServiceConfigurationEvent event = new APIUpdateExternalServiceConfigurationEvent(msg.getId()); + ExternalServiceConfigurationVO vo = dbf.findByUuid(msg.getUuid(), ExternalServiceConfigurationVO.class); + final String syncKey = vo != null ? vo.getServiceType() : msg.getUuid(); + + thdf.chainSubmit(new ChainTask(msg) { + @Override + public void run(SyncTaskChain chain) { + updateExternalServiceConfiguration(msg, event, new Completion(chain) { + @Override + public void success() { + bus.publish(event); + chain.next(); + } + + @Override + public void fail(ErrorCode errorCode) { + event.setError(errorCode); + bus.publish(event); + chain.next(); + } + }); + } + + @Override + public String getSyncSignature() { + return String.format("create-update-delete-external-service-configuration-%s", syncKey); + } + + @Override + public String getName() { + return String.format("update-external-service-configuration-%s", msg.getUuid()); + } + }); + } + + private void updateExternalServiceConfiguration(APIUpdateExternalServiceConfigurationMsg msg, APIUpdateExternalServiceConfigurationEvent evt, Completion completion) { + ExternalServiceConfigurationVO vo = dbf.findByUuid(msg.getUuid(), ExternalServiceConfigurationVO.class); + + if (vo == null) { + completion.fail(operr("unable to find external service configuration with uuid [%s]", msg.getUuid())); + return; + } + + if (msg.getDescription() != null) { + vo.setDescription(msg.getDescription()); + } + vo = dbf.updateAndRefresh(vo); + + evt.setInventory(ExternalServiceConfigurationInventory.valueOf(vo)); + completion.success(); + } + + private void handle(APIDeleteExternalServiceConfigurationMsg msg) { + APIDeleteExternalServiceConfigurationEvent event = new APIDeleteExternalServiceConfigurationEvent(msg.getId()); + ExternalServiceConfigurationVO vo = dbf.findByUuid(msg.getUuid(), ExternalServiceConfigurationVO.class); + final String syncKey = vo != null ? vo.getServiceType() : msg.getUuid(); + + thdf.chainSubmit(new ChainTask(msg) { + @Override + public void run(SyncTaskChain chain) { + deleteExternalServiceConfiguration(msg, event, new Completion(chain) { + @Override + public void success() { + bus.publish(event); + chain.next(); + } + + @Override + public void fail(ErrorCode errorCode) { + event.setError(errorCode); + bus.publish(event); + chain.next(); + } + }); + } + + @Override + public String getSyncSignature() { + return String.format("create-update-delete-external-service-configuration-%s", syncKey); + } + + @Override + public String getName() { + return String.format("delete-external-service-configuration-%s", msg.getUuid()); + } + }); + } + + private void deleteExternalServiceConfiguration(APIDeleteExternalServiceConfigurationMsg msg, APIDeleteExternalServiceConfigurationEvent evt, Completion completion) { + // delete db record + ExternalServiceConfigurationVO vo = dbf.findByUuid(msg.getUuid(), ExternalServiceConfigurationVO.class); + String serviceType; + if (vo != null) { + serviceType = vo.getServiceType(); + dbf.remove(vo); + } else { + completion.success(); + return; + } + + applyExternalServiceConfigurationToAllNodes(serviceType, new ReturnValueCompletion>(completion) { + @Override + public void success(List returnValue) { + completion.success(); + } + + @Override + public void fail(ErrorCode errorCode) { + completion.fail(errorCode); + } + }); + } + + private void applyExternalServiceConfigurationToAllNodes(String serviceType, ReturnValueCompletion> completion) { + final List results = Collections.synchronizedList(new ArrayList<>()); + + FlowChain chain = new SimpleFlowChain(); + chain.setName("apply-external-service-configuration-to-all-nodes"); + chain.then(new Flow() { + String __name__ = "apply-external-service-configuration"; + + @Override + public void run(FlowTrigger trigger, Map data) { + List mnUuids = Q.New(ManagementNodeVO.class).select(ManagementNodeVO_.uuid).listValues(); + + final ErrorCode[] errorCode = new ErrorCode[1]; + new While<>(mnUuids).each((mnUuid, whileCompletion) -> { + ApplyExternalServiceConfigurationMsg amsg = new ApplyExternalServiceConfigurationMsg(); + amsg.setServiceType(serviceType); + bus.makeServiceIdByManagementNodeId(amsg, SERVICE_ID, mnUuid); + bus.send(amsg, new CloudBusCallBack(whileCompletion) { + @Override + public void run(MessageReply reply) { + ApplyExternalConfigurationResult result = new ApplyExternalConfigurationResult(); + result.setManagementNodeUuid(mnUuid); + results.add(result); + + if (!reply.isSuccess()) { + result.setErrorCode(reply.getError()); + errorCode[0] = reply.getError(); + whileCompletion.allDone(); + return; + } + whileCompletion.done(); + } + }); + }).run(new WhileDoneCompletion(trigger) { + @Override + public void done(ErrorCodeList errorCodeList) { + if (errorCode[0] != null) { + trigger.fail(errorCode[0]); + return; + } + trigger.next(); + } + }); + } + + @Override + public void rollback(FlowRollback trigger, Map data) { + trigger.rollback(); + } + }).done(new FlowDoneHandler(completion) { + @Override + public void handle(Map data) { + completion.success(results); + } + }).error(new FlowErrorHandler(completion) { + @Override + public void handle(ErrorCode errCode, Map data) { + completion.fail(errCode); + } + }).start(); + } + + private void handle(ApplyExternalServiceConfigurationMsg msg) { + ApplyExternalServiceConfigurationReply reply = new ApplyExternalServiceConfigurationReply(); + + regenerateExternalServiceConfiguration(msg.getServiceType(), new ReturnValueCompletion(msg) { + @Override + public void success(String returnValue) { + reply.setValue(returnValue); + reply.setManagementNodeUuid(Platform.getManagementServerId()); + bus.reply(msg, reply); + } + + @Override + public void fail(ErrorCode errorCode) { + reply.setError(errorCode); + bus.reply(msg, reply); + } + }); + } + + private void regenerateExternalServiceConfiguration(String serviceType, ReturnValueCompletion completion) { + if (CoreGlobalProperty.UNIT_TEST_ON) { + completion.success(serviceType); + return; + } + for (ExternalService service : services.values()) { + if (serviceType.equals(service.getServiceType())) { + try{ + service.externalConfig(serviceType); + completion.success(serviceType); + } catch (Exception e) { + completion.fail(operr("failed to apply external service configuration for type [%s]: %s", serviceType, e.getMessage())); + } + return; + } + } + completion.fail(operr("unable to find external service type [%s]", serviceType)); + } + @Override public String getId() { return bus.makeLocalServiceId(SERVICE_ID); diff --git a/core/src/main/java/org/zstack/core/progress/ProgressReportService.java b/core/src/main/java/org/zstack/core/progress/ProgressReportService.java index bab9acc163a..dc9d93ea241 100755 --- a/core/src/main/java/org/zstack/core/progress/ProgressReportService.java +++ b/core/src/main/java/org/zstack/core/progress/ProgressReportService.java @@ -225,6 +225,9 @@ private TaskProgressInventory inventory(TaskProgressVO vo) { if (!StringUtils.isEmpty(vo.getArguments())) { inv.setArguments(vo.getArguments()); } + + inv.setProgressDetail(LongJobProgressDetailBuilder.fromTaskProgressVO(vo)); + return inv; } diff --git a/core/src/main/java/org/zstack/core/thread/AbstractCoalesceQueue.java b/core/src/main/java/org/zstack/core/thread/AbstractCoalesceQueue.java new file mode 100644 index 00000000000..579a327160d --- /dev/null +++ b/core/src/main/java/org/zstack/core/thread/AbstractCoalesceQueue.java @@ -0,0 +1,198 @@ +package org.zstack.core.thread; + +import org.springframework.beans.factory.annotation.Autowire; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Configurable; +import org.zstack.header.core.AbstractCompletion; +import org.zstack.header.core.Completion; +import org.zstack.header.core.ReturnValueCompletion; +import org.zstack.header.errorcode.ErrorCode; +import org.zstack.utils.Utils; +import org.zstack.utils.logging.CLogger; + +import static org.zstack.core.Platform.operr; +import static org.zstack.utils.clouderrorcode.CloudOperationsErrorCode.ORG_ZSTACK_CORE_THREAD_10003; +import static org.zstack.utils.clouderrorcode.CloudOperationsErrorCode.ORG_ZSTACK_CORE_THREAD_10004; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.concurrent.ConcurrentHashMap; +import java.util.stream.Collectors; + +/** + * Base implementation for coalesce queues. + * + * @param Request Item Type + * @param Batch Execution Result Type + * @param Single Request Result Type + */ +@Configurable(preConstruction = true, autowire = Autowire.BY_TYPE) +public abstract class AbstractCoalesceQueue { + private static final CLogger logger = Utils.getLogger(AbstractCoalesceQueue.class); + + @Autowired + private ThreadFacade thdf; + + private final ConcurrentHashMap signatureQueues = new ConcurrentHashMap<>(); + + protected class PendingRequest { + final T item; + final AbstractCompletion completion; + + PendingRequest(T item, AbstractCompletion completion) { + this.item = item; + this.completion = completion; + } + + @SuppressWarnings("unchecked") + void notifySuccess(V result) { + if (completion == null) { + return; + } + + if (completion instanceof ReturnValueCompletion) { + ((ReturnValueCompletion) completion).success(result); + } else if (completion instanceof Completion) { + ((Completion) completion).success(); + } + } + + void notifyFailure(ErrorCode errorCode) { + if (completion == null) { + return; + } + + if (completion instanceof ReturnValueCompletion) { + ((ReturnValueCompletion) completion).fail(errorCode); + } else if (completion instanceof Completion) { + ((Completion) completion).fail(errorCode); + } + } + } + + private class SignatureQueue { + final String syncSignature; + List pendingList = Collections.synchronizedList(new ArrayList<>()); + + SignatureQueue(String syncSignature) { + this.syncSignature = syncSignature; + } + + synchronized List takeAll() { + List toProcess = pendingList; + pendingList = Collections.synchronizedList(new ArrayList<>()); + return toProcess; + } + + synchronized void add(PendingRequest request) { + pendingList.add(request); + } + + synchronized boolean isEmpty() { + return pendingList.isEmpty(); + } + } + + protected abstract String getName(); + + // Changed to take AbstractCompletion, subclasses cast it to specific type + protected abstract void executeBatch(List items, AbstractCompletion completion); + + protected abstract AbstractCompletion createBatchCompletion(String syncSignature, List requests, SyncTaskChain chain); + + protected abstract V calculateResult(T item, R batchResult); + + protected final void handleSuccess(String syncSignature, List requests, R batchResult, SyncTaskChain chain) { + for (PendingRequest req : requests) { + try { + V singleResult = calculateResult(req.item, batchResult); + req.notifySuccess(singleResult); + } catch (Throwable t) { + logger.warn(String.format("[%s] failed to calculate result for item %s", getName(), req.item), t); + req.notifyFailure(operr(ORG_ZSTACK_CORE_THREAD_10003, "failed to calculate result: %s", t.getMessage())); + } + } + cleanup(syncSignature); + chain.next(); + } + + protected final void handleFailure(String syncSignature, List requests, ErrorCode errorCode, SyncTaskChain chain) { + for (PendingRequest req : requests) { + req.notifyFailure(errorCode); + } + cleanup(syncSignature); + chain.next(); + } + + void setThreadFacade(ThreadFacade thdf) { + this.thdf = thdf; + } + + protected final void submitRequest(String syncSignature, T item, AbstractCompletion completion) { + doSubmit(syncSignature, new PendingRequest(item, completion)); + } + + private void doSubmit(String syncSignature, PendingRequest request) { + SignatureQueue queue = signatureQueues.computeIfAbsent(syncSignature, SignatureQueue::new); + queue.add(request); + + thdf.chainSubmit(new ChainTask(null) { + @Override + public String getSyncSignature() { + return String.format("coalesce-queue-%s-%s", AbstractCoalesceQueue.this.getName(), syncSignature); + } + + @Override + public void run(SyncTaskChain chain) { + List requests = queue.takeAll(); + + if (requests.isEmpty()) { + chain.next(); + return; + } + + String name = getName(); + logger.debug(String.format("[%s] coalescing %d requests for signature[%s]", + name, requests.size(), syncSignature)); + + + // Create the specific completion type (Completion or ReturnValueCompletion) + AbstractCompletion batchCompletion = createBatchCompletion(syncSignature, requests, chain); + + // Execute batch with the direct completion object + List items = requests.stream().map(req -> req.item).collect(Collectors.toList()); + + /** *(.., AbstractCompletion, ..) is not AsyncSafeAspect's pointcut, but it will call + * executeBatch(.., Completion/ReturnValueCompletion) which is pointcut, + * so we do not need try-catch here. + */ + executeBatch(items, batchCompletion); + } + + @Override + public String getName() { + return String.format("%s-coalesced-batch-%s", AbstractCoalesceQueue.this.getName(), syncSignature); + } + + @Override + protected int getSyncLevel() { + return 1; + } + }); + } + + private void cleanup(String syncSignature) { + signatureQueues.computeIfPresent(syncSignature, (k, queue) -> { + if (queue.isEmpty()) { + return null; + } + return queue; + }); + } + + // For testing + int getActiveQueueCount() { + return signatureQueues.size(); + } +} diff --git a/core/src/main/java/org/zstack/core/thread/CoalesceQueue.java b/core/src/main/java/org/zstack/core/thread/CoalesceQueue.java new file mode 100644 index 00000000000..45473efb751 --- /dev/null +++ b/core/src/main/java/org/zstack/core/thread/CoalesceQueue.java @@ -0,0 +1,46 @@ +package org.zstack.core.thread; + +import org.zstack.header.core.AbstractCompletion; +import org.zstack.header.core.Completion; +import org.zstack.header.errorcode.ErrorCode; + +import java.util.List; + +/** + * A coalesce queue for requests that do NOT expect a return value. + * + * @param Request Item Type + */ +public abstract class CoalesceQueue extends AbstractCoalesceQueue { + + public void submit(String syncSignature, T item, Completion completion) { + submitRequest(syncSignature, item, completion); + } + + protected abstract void executeBatch(List items, Completion completion); + + @Override + protected final void executeBatch(List items, AbstractCompletion batchCompletion) { + executeBatch(items, (Completion) batchCompletion); + } + + @Override + protected final AbstractCompletion createBatchCompletion(String syncSignature, List requests, SyncTaskChain chain) { + return new Completion(chain) { + @Override + public void success() { + handleSuccess(syncSignature, requests, null, chain); + } + + @Override + public void fail(ErrorCode errorCode) { + handleFailure(syncSignature, requests, errorCode, chain); + } + }; + } + + @Override + protected final Void calculateResult(T item, Void batchResult) { + return null; + } +} diff --git a/core/src/main/java/org/zstack/core/thread/ReturnValueCoalesceQueue.java b/core/src/main/java/org/zstack/core/thread/ReturnValueCoalesceQueue.java new file mode 100644 index 00000000000..bf52cc93f31 --- /dev/null +++ b/core/src/main/java/org/zstack/core/thread/ReturnValueCoalesceQueue.java @@ -0,0 +1,43 @@ +package org.zstack.core.thread; + +import org.zstack.header.core.AbstractCompletion; +import org.zstack.header.core.ReturnValueCompletion; +import org.zstack.header.errorcode.ErrorCode; + +import java.util.List; + +/** + * A coalesce queue for requests that expect a return value. + * + * @param Request Item Type + * @param Batch Execution Result Type + * @param Single Request Result Type + */ +public abstract class ReturnValueCoalesceQueue extends AbstractCoalesceQueue { + + public void submit(String syncSignature, T item, ReturnValueCompletion completion) { + submitRequest(syncSignature, item, completion); + } + + protected abstract void executeBatch(List items, ReturnValueCompletion completion); + + @Override + protected final void executeBatch(List items, AbstractCompletion batchCompletion) { + executeBatch(items, (ReturnValueCompletion) batchCompletion); + } + + @Override + protected final AbstractCompletion createBatchCompletion(String syncSignature, List requests, SyncTaskChain chain) { + return new ReturnValueCompletion(chain) { + @Override + public void success(R batchResult) { + handleSuccess(syncSignature, requests, batchResult, chain); + } + + @Override + public void fail(ErrorCode errorCode) { + handleFailure(syncSignature, requests, errorCode, chain); + } + }; + } +} diff --git a/docs/modules/network/nav.adoc b/docs/modules/network/nav.adoc index 53e42d44f22..19dbb6c06b1 100644 --- a/docs/modules/network/nav.adoc +++ b/docs/modules/network/nav.adoc @@ -2,5 +2,6 @@ ** xref:networkResource/networkResource.adoc[] *** xref:networkResource/L2Network.adoc[] *** xref:networkResource/L3Network.adoc[] + *** xref:networkResource/l3Ipam.adoc[] *** xref:networkResource/VpcRouter.adoc[] ** xref:networkService/networkService.adoc[] \ No newline at end of file diff --git a/docs/modules/network/pages/networkResource/l3Ipam.adoc b/docs/modules/network/pages/networkResource/l3Ipam.adoc new file mode 100644 index 00000000000..8f0d00fb117 --- /dev/null +++ b/docs/modules/network/pages/networkResource/l3Ipam.adoc @@ -0,0 +1,201 @@ += 三层网络IPAM + +ZStack IPAM 负责管理三层网络(L3)的 IP 地址分配与回收,支持以下三种方式: + +. 自动分配:ZStack 云平台根据 L3 网络配置的 IP range 自动分配。 +. 手动分配:用户在创建云主机时手动指定 IP 地址。 +. QGA 获取:通过 QEMU Guest Agent(QGA)从云主机内部读取并上报 IP 地址。 + +== 自动分配 + +自动分配需要满足以下两个条件: + +. L3 网络已配置 IP range。 +. L3 网络已启用 DHCP 服务。 ++ +这是历史遗留行为:扁平网络使用 DHCP 服务作为自动分配功能的开关,其他网络类型不受影响。 + +系统根据用户输入的 L3 网络 UUID 以及可选的 IP 地址,按照分配算法分配一个可用地址。返回的地址信息包括:IP 地址、掩码(或前缀长度)、网关。 + +=== 自动分配算法 + +* 随机分配:从可用地址池中随机选择一个 IP 地址。 +* 顺序分配:从可用地址池中按顺序选择一个 IP 地址。 +* 循环分配:从可用地址池中按顺序分配,分配到最后一个 IP 后回到第一个 IP 继续分配。 + +=== 当前状况 + +Cloud 5.5 版本中: + +. 扁平网络有三种情况:无 IP range、有 IP range 且未启用 DHCP、有 IP range 且已启用 DHCP。 +. 公有网络与 VPC 网络有两种情况:有 IP range 且未启用 DHCP、有 IP range 且已启用 DHCP。 +. 管理网络与流量网络仅有一种情况:有 IP range 且未启用 DHCP。 + +=== 工作时机 + +以下操作会触发自动分配: + +. 创建云主机(`APICreateVmInstanceMsg`) +. 云主机添加网卡(`APIAttachL3NetworkToVmMsg`, `APICreateVmNicMsg`) +. 修改云主机 IP(`APISetVmStaticIpMsg`, `APIChangeVmNicNetworkMsg`) +. 创建 appliance VM(`APICreateVpcVRouterMsg`, `APICreateSlbInstanceMsg`, `APICreateNfvInstMsg`) +. 创建 VIP(`APICreateVipMsg`) + +== 手动分配 + +手动指定仅对云主机生效,对 appliance VM 不生效。 + +在前述场景 1、2、3 中,用户可以指定 IP 地址,分为两种情况: + +. 指定 IP 在 IP range 之内:后端仍执行自动分配流程。 +. 指定 IP 不在 IP range 之内:按手动分配流程处理。 +.. 如果指定 IP 不在 L3 CIDR 内,必须指定掩码,网关可选。 +.. 如果指定 IP 在 L3 CIDR 内,可不指定掩码和网关;如指定,必须与 L3 CIDR 一致。 + +=== 工作时机 + +. 5.5.12 之前:扁平网络在“无 IP range”或“有 IP range 但未启用 DHCP”两种情况下,允许指定不在 IP range 内的地址。 +. 5.5.12 及以后:任意网络都可通过修改云主机 IP(`APISetVmStaticIpMsg`, `APIChangeVmNicNetworkMsg`)设置不在 IP range 内的地址。 + +如果指定 IP 在 IP range 之外、但在 L3 CIDR 之内,则掩码和网关可不指定,系统会自动从 L3 CIDR 推导。 + +如果指定 IP 在 L3 CIDR 之外,用户必须同时提供 IP 与掩码(或前缀长度);若为默认网卡,还必须指定网关,且网关必须位于 L3 CIDR 内。 + +== QGA 获取 + +该方式需要开启全局配置 `VmGlobalConfig.ENABLE_VM_INTERNAL_IP_OVERWRITE`(默认值为 `false`)。 + +ZStack KVM Agent 会定期通过 QGA 从云主机内部读取 IP 地址。仅在扁平网络且无 IP range 时,会将读取到的 IP 分配给云主机;其他网络类型不受影响。 + +在其他情况下,如果 QGA 读取到的 IP 与云主机当前 IP 冲突,系统会产生告警。 + +== 配置虚拟机 Guest OS 的 IP 地址 + +配置虚拟机 Guest OS 的 IP 地址有三种方式: + +. DHCP:通过 DHCP 服务器动态分配 IP 地址。 +. Cloud-init:在云主机创建时通过 Cloud-init 预配置 IP 地址。 +. QGA:通过 QEMU Guest Agent 配置云主机内部网络参数。 + +=== DHCP + +ZStack 会在每个物理机启动分布式 DHCP Server。云主机启动后通过 `dhclient` 获取 IP 地址、DNS 等参数。 + +=== Cloud-init + +ZStack 会在每个物理机启动分布式 Userdata Server。云主机启动后通过 Cloud-init 获取 IP 地址、DNS 等参数。 + +=== QGA + +云主机安装 ZStack Guest Agent 后,系统在检测到 Guest Agent 首次启动时,会通过 QGA 配置云主机的 IP、DNS 等参数。 + +当用户在 UI 手动修改 IP(`APISetVmStaticIpMsg`, `APIChangeVmNicNetworkMsg`)后,后端 API 会触发一次 Guest OS 网络参数下发流程。 + +QGA 下发的参数包括: + +* IP 地址 +* 掩码或前缀长度 +* 网关 +* DNS 服务器地址 +* MTU +* Hostname + +用户可通过全局配置 `GuestToolsGlobalProperty.GUESTTOOLS_VM_PORT_CONFIGFIELDS` 限制下发字段。 + +== 网络服务 + +当网卡地址不在 L3 IP range 内时,可分为“在 L3 CIDR 内”和“在 L3 CIDR 外”两种情况。 + +=== 在 L3 CIDR 内 + +该情况与“IP 在 L3 IP range 内”一致,网络服务不受影响。 + +=== 在 L3 CIDR 外 + +==== 安全组 + +安全组规则本身不依赖网卡 IP 是否位于 L3 CIDR 内,但需要用户谨慎设计规则,避免不符合预期。 + +==== DHCP + +如果网卡所属网络无 IP range,则不提供 DHCP 服务。 + +如果网卡所属网络有 IP range,ZStack 会启动 DHCP 服务,且 `dnsmasq` 配置要求指定一个 IP CIDR。 + +当网卡 IP 不在 DHCP 服务对应的 IP CIDR 内时,DHCP 模块下发配置时会过滤掉 CIDR 外地址。 + +==== EIP + +对于扁平网络,EIP 功能不受影响,可继续创建。 + +对于 VPC 网络,若 EIP 绑定的私网地址不在 L3 CIDR 内,VPC 路由器无法路由,网络不通。 + +为保证一致性,EIP 不能绑定 IP 不在 L3 CIDR 内的网卡;`APIGetEipAttachableVmNicsMsg` 也不会返回该类网卡。 + +==== Port Forwarding + +Port Forwarding 仅用于 VPC 网络。与 EIP 类似,若网卡 IP 不在 L3 CIDR 内,VPC 路由器无法路由,网络不通。 + +Port Forwarding 不能绑定 IP 不在 L3 CIDR 内的网卡;`APIGetPortForwardingAttachableVmNicsMsg` 也不会返回该类网卡。 + +==== Load Balancer + +与 EIP 类似,若网卡 IP 不在 L3 CIDR 内,网络不通。 + +`APIAddVmNicToLoadBalancerMsg`、`APIAddBackendServerToServerGroupMsg` 不能绑定 IP 不在 L3 CIDR 内的网卡。 + +`APIGetCandidateVmNicsForLoadBalancerServerGroupMsg`、`APIGetCandidateVmNicsForLoadBalancerMsg` 也不会返回该类网卡。 + +== 代码细节 + +=== APISetVmStaticIpMsg + +该 API 通过成员字段配置云主机 IP、掩码、网关等参数,并进行完整性校验: + +* 用户输入掩码与网关:以用户输入为准。 +* 用户仅输入网关:若 IP 与网关均在 L3 CIDR 内,使用 L3 CIDR 掩码;否则报错。 +* 用户仅输入掩码:若与 L3 CIDR 掩码一致,使用 L3 CIDR 网关;否则若网卡为默认网卡或唯一网卡,报错;否则使用输入掩码,网关置空。 +* 用户未输入掩码和网关:若网卡存在相同 IP 版本地址,且输入 IP 在旧 IP 的掩码与网关组成 CIDR 内,则复用旧掩码与网关;否则若 IP 在 L3 CIDR 内,使用 L3 CIDR 的掩码与网关;否则报错。 +* IPv6 与 IPv4 的逻辑一致。 +* 对 DNS 参数:若 UI 传递 `NULL`,后端保持旧 DNS 参数不变。 +* 对 DNS 参数:若 UI 传递 `""` 或 `[]`,后端删除旧 DNS。 +* 其他情况仅允许传递合法 DNS 列表,后端会先删除旧 DNS,再配置新 DNS。 + +=== APIChangeVmNicNetworkMsg + +该 API 通过 System Tags 配置云主机 IP、掩码、网关等参数,并进行完整性校验: + +* 用户输入掩码与网关:以用户输入为准。 +* 用户仅输入网关:若 IP 与网关均在 L3 CIDR 内,使用 L3 CIDR 掩码;否则报错。 +* 用户仅输入掩码:若与 L3 CIDR 掩码一致,使用 L3 CIDR 网关;否则若网卡为默认网卡或唯一网卡,报错;否则使用输入掩码,网关置空。 +* 用户未输入掩码和网关:若 IP 在 L3 CIDR 内,使用 L3 CIDR 掩码与网关;否则报错。 +* IPv6 与 IPv4 的逻辑一致。 +* 对 DNS 参数:若 UI 传递 `NULL`,后端保持旧 DNS 参数不变。 +* 对 DNS 参数:若 UI 传递 `""` 或 `[]`,后端删除旧 DNS。 +* 其他情况仅允许传递合法 DNS 列表,后端会先删除旧 DNS,再配置新 DNS。 + +=== APICreateVmInstanceMsg + +逻辑与 `APIChangeVmNicNetworkMsg` 相同。 + +=== APIGetL3NetworkIpStatisticMsg + +不统计位于 IP range 之外的 IP 地址。 + +=== APIAddIpRangeMsg + +系统允许给云主机设置不在 IP range 内的 IP,因此添加 IP range 时可能覆盖已分配 IP。此时会将这些已分配地址归属到新加入的 IP range。 + +=== APIAddReservedIpRangeMsg + +该 API 不仅会添加 `ReservedIpRangeVO`,还会将 `ReservedIpRangeVO` 与 IP range 重叠的地址写入 `UsedIpVO`。 + +[source,java] +---- +vo.setUsedFor(IpAllocatedReason.Reserved.toString()); +---- + +=== APICheckIpAvailabilityMsg + +在 5.5.12 之前,该 API 在扁平网络未启用 DHCP 的情况下会跳过 IP range 检查;该行为保持不变。 + diff --git a/docs/modules/network/pages/networkResource/networkResource.adoc b/docs/modules/network/pages/networkResource/networkResource.adoc index 9aa66ce7341..3b567e4eb66 100644 --- a/docs/modules/network/pages/networkResource/networkResource.adoc +++ b/docs/modules/network/pages/networkResource/networkResource.adoc @@ -2,4 +2,5 @@ * xref:networkResource/L2Network.adoc[] * xref:networkResource/L3Network.adoc[] +* xref:networkResource/l3Ipam.adoc[] * xref:networkResource/VpcRouter.adoc[] \ No newline at end of file diff --git a/externalservice/src/main/java/org/zstack/externalservice/cronjob/CronJobImpl.java b/externalservice/src/main/java/org/zstack/externalservice/cronjob/CronJobImpl.java index 38e061312d4..1ed1d889cce 100755 --- a/externalservice/src/main/java/org/zstack/externalservice/cronjob/CronJobImpl.java +++ b/externalservice/src/main/java/org/zstack/externalservice/cronjob/CronJobImpl.java @@ -34,6 +34,12 @@ public String getName() { return String.format("cron-job-on-machine-%s", Platform.getManagementServerIp()); } + + @Override + public String getServiceType() { + return "CronJob"; + } + @Override public void start() { if (isAlive()) { diff --git a/header/src/main/java/org/zstack/header/allocator/AllocateHostMsg.java b/header/src/main/java/org/zstack/header/allocator/AllocateHostMsg.java index 23869aa1327..11f911dff0b 100755 --- a/header/src/main/java/org/zstack/header/allocator/AllocateHostMsg.java +++ b/header/src/main/java/org/zstack/header/allocator/AllocateHostMsg.java @@ -31,6 +31,7 @@ public class AllocateHostMsg extends NeedReplyMessage { private long oldMemoryCapacity = 0; private AllocationScene allocationScene; private String architecture; + private String accountUuid; public List> getOptionalPrimaryStorageUuids() { return optionalPrimaryStorageUuids; @@ -211,4 +212,8 @@ public String getArchitecture() { public void setArchitecture(String architecture) { this.architecture = architecture; } + + public String getAccountUuid() { return accountUuid; } + + public void setAccountUuid(String accountUuid) { this.accountUuid = accountUuid; } } diff --git a/header/src/main/java/org/zstack/header/allocator/HostAllocatorSpec.java b/header/src/main/java/org/zstack/header/allocator/HostAllocatorSpec.java index c8d8ddc3af8..e0ed69c47a1 100755 --- a/header/src/main/java/org/zstack/header/allocator/HostAllocatorSpec.java +++ b/header/src/main/java/org/zstack/header/allocator/HostAllocatorSpec.java @@ -37,6 +37,7 @@ public class HostAllocatorSpec { private long oldMemoryCapacity = 0; private AllocationScene allocationScene; private String architecture; + private String accountUuid; public AllocationScene getAllocationScene() { return allocationScene; @@ -161,7 +162,13 @@ public List getL3NetworkUuids() { } return l3NetworkUuids; } + public String getAccountUuid() { + return accountUuid; + } + public void setAccountUuid(String accountUuid) { + this.accountUuid = accountUuid; + } public void setL3NetworkUuids(List l3NetworkUuids) { this.l3NetworkUuids = l3NetworkUuids; } @@ -250,6 +257,7 @@ public static HostAllocatorSpec fromAllocationMsg(AllocateHostMsg msg) { msg.getOptionalPrimaryStorageUuids().forEach(spec::addOptionalPrimaryStorageUuids); spec.setAllocationScene(msg.getAllocationScene()); spec.setArchitecture(msg.getArchitecture()); + spec.setAccountUuid(msg.getAccountUuid()); if (msg.getSystemTags() != null && !msg.getSystemTags().isEmpty()){ spec.setSystemTags(new ArrayList(msg.getSystemTags())); } diff --git a/header/src/main/java/org/zstack/header/cluster/APICreateClusterMsg.java b/header/src/main/java/org/zstack/header/cluster/APICreateClusterMsg.java index 18d1727a07f..7d740b7d152 100755 --- a/header/src/main/java/org/zstack/header/cluster/APICreateClusterMsg.java +++ b/header/src/main/java/org/zstack/header/cluster/APICreateClusterMsg.java @@ -70,7 +70,7 @@ public class APICreateClusterMsg extends APICreateMessage implements CreateClust * - Simulator * - baremetal */ - @APIParam(validValues = {"KVM", "Simulator", "baremetal", "baremetal2", "xdragon"}) + @APIParam(validValues = {"KVM", "Simulator", "baremetal", "baremetal2", "xdragon", "baremetal2Dpu"}) private String hypervisorType; /** * @desc see field 'type' of :ref:`ClusterInventory` for details diff --git a/header/src/main/java/org/zstack/header/cluster/APICreateClusterMsgDoc_zh_cn.groovy b/header/src/main/java/org/zstack/header/cluster/APICreateClusterMsgDoc_zh_cn.groovy index 90c01a79fa8..b4faf3d122e 100644 --- a/header/src/main/java/org/zstack/header/cluster/APICreateClusterMsgDoc_zh_cn.groovy +++ b/header/src/main/java/org/zstack/header/cluster/APICreateClusterMsgDoc_zh_cn.groovy @@ -56,7 +56,7 @@ doc { type "String" optional false since "0.6" - values ("KVM","Simulator","baremetal","baremetal2","xdragon") + values ("KVM","Simulator","baremetal","baremetal2","xdragon","baremetal2Dpu") } column { name "type" diff --git a/header/src/main/java/org/zstack/header/core/external/service/APIAddExternalServiceConfigurationEvent.java b/header/src/main/java/org/zstack/header/core/external/service/APIAddExternalServiceConfigurationEvent.java new file mode 100644 index 00000000000..7341efed929 --- /dev/null +++ b/header/src/main/java/org/zstack/header/core/external/service/APIAddExternalServiceConfigurationEvent.java @@ -0,0 +1,34 @@ +package org.zstack.header.core.external.service; + +import org.zstack.header.message.APIEvent; +import org.zstack.header.rest.RestResponse; + +/** + * @Author: ya.wang + * @Date: 1/15/26 12:48 AM + */ +@RestResponse(allTo = "inventory") +public class APIAddExternalServiceConfigurationEvent extends APIEvent { + + private ExternalServiceConfigurationInventory inventory; + + public APIAddExternalServiceConfigurationEvent() {} + + public APIAddExternalServiceConfigurationEvent(String apiId) { super(apiId);} + + public void setInventory(ExternalServiceConfigurationInventory inventory) {this.inventory = inventory;} + + public ExternalServiceConfigurationInventory getInventory() {return inventory;} + + public static APIAddExternalServiceConfigurationEvent __example__() { + APIAddExternalServiceConfigurationEvent event = new APIAddExternalServiceConfigurationEvent(); + ExternalServiceConfigurationInventory inv = new ExternalServiceConfigurationInventory(); + + inv.setUuid(uuid()); + inv.setServiceType("Prometheus2"); + inv.setConfiguration("{}"); + event.setInventory(inv); + + return event; + } +} diff --git a/header/src/main/java/org/zstack/header/core/external/service/APIAddExternalServiceConfigurationEventDoc_zh_cn.groovy b/header/src/main/java/org/zstack/header/core/external/service/APIAddExternalServiceConfigurationEventDoc_zh_cn.groovy new file mode 100644 index 00000000000..db890f8acf4 --- /dev/null +++ b/header/src/main/java/org/zstack/header/core/external/service/APIAddExternalServiceConfigurationEventDoc_zh_cn.groovy @@ -0,0 +1,32 @@ +package org.zstack.header.core.external.service + +import org.zstack.header.core.external.service.ExternalServiceConfigurationInventory +import org.zstack.header.errorcode.ErrorCode + +doc { + + title "添加外部服务配置返回" + + ref { + name "inventory" + path "org.zstack.header.core.external.service.APIAddExternalServiceConfigurationEvent.inventory" + desc "外部服务配置详情" + type "ExternalServiceConfigurationInventory" + since "5.5.12" + clz ExternalServiceConfigurationInventory.class + } + field { + name "success" + desc "操作是否成功" + type "boolean" + since "5.5.12" + } + ref { + name "error" + path "org.zstack.header.core.external.service.APIAddExternalServiceConfigurationEvent.error" + desc "错误码,若不为null,则表示操作失败, 操作成功时该字段为null",false + type "ErrorCode" + since "5.5.12" + clz ErrorCode.class + } +} diff --git a/header/src/main/java/org/zstack/header/core/external/service/APIAddExternalServiceConfigurationMsg.java b/header/src/main/java/org/zstack/header/core/external/service/APIAddExternalServiceConfigurationMsg.java new file mode 100644 index 00000000000..a6983f464db --- /dev/null +++ b/header/src/main/java/org/zstack/header/core/external/service/APIAddExternalServiceConfigurationMsg.java @@ -0,0 +1,66 @@ +package org.zstack.header.core.external.service; + +import org.springframework.http.HttpMethod; +import org.zstack.header.message.APICreateMessage; +import org.zstack.header.message.APIEvent; +import org.zstack.header.message.APIMessage; +import org.zstack.header.message.APIParam; +import org.zstack.header.other.APIAuditor; +import org.zstack.header.rest.RestRequest; + +/** + * @Author: ya.wang + * @Date: 1/15/26 12:43 AM + */ +@RestRequest( + path = "/external/service/configuration", + method = HttpMethod.POST, + parameterName = "params", + responseClass = APIAddExternalServiceConfigurationEvent.class +) +public class APIAddExternalServiceConfigurationMsg extends APICreateMessage implements APIAuditor { + @APIParam + private String externalServiceType; + @APIParam(maxLength = 65535) + private String configuration; + @APIParam(maxLength = 2048, required = false) + private String description; + + public String getExternalServiceType() { + return externalServiceType; + } + + public void setExternalServiceType(String externalServiceType) { + this.externalServiceType = externalServiceType; + } + + public String getConfiguration() { + return configuration; + } + + public void setConfiguration(String configuration) { + this.configuration = configuration; + } + + public String getDescription() { + return description; + } + + public void setDescription(String description) { + this.description = description; + } + + @Override + public Result audit(APIMessage msg, APIEvent rsp) { + APIAddExternalServiceConfigurationEvent evt = (APIAddExternalServiceConfigurationEvent) rsp; + return new Result(rsp.isSuccess() ? evt.getInventory().getUuid(): "", ExternalServiceConfigurationVO.class); + } + + public static APIAddExternalServiceConfigurationMsg __example__() { + APIAddExternalServiceConfigurationMsg msg = new APIAddExternalServiceConfigurationMsg(); + msg.setExternalServiceType("Prometheus2"); + msg.setConfiguration("{}"); + msg.setDescription("description"); + return msg; + } +} \ No newline at end of file diff --git a/header/src/main/java/org/zstack/header/core/external/service/APIAddExternalServiceConfigurationMsgDoc_zh_cn.groovy b/header/src/main/java/org/zstack/header/core/external/service/APIAddExternalServiceConfigurationMsgDoc_zh_cn.groovy new file mode 100644 index 00000000000..2076550ce46 --- /dev/null +++ b/header/src/main/java/org/zstack/header/core/external/service/APIAddExternalServiceConfigurationMsgDoc_zh_cn.groovy @@ -0,0 +1,94 @@ +package org.zstack.header.core.external.service + +import org.zstack.header.core.external.service.APIAddExternalServiceConfigurationEvent + +doc { + title "新建外部服务配置" + + category "externalService" + + desc """新建外部服务配置""" + + rest { + request { + url "POST /v1/external/service/configuration" + + header (Authorization: 'OAuth the-session-uuid') + + clz APIAddExternalServiceConfigurationMsg.class + + desc """""" + + params { + + column { + name "externalServiceType" + enclosedIn "params" + desc "外部服务类型, 例如 Prometheus2" + location "body" + type "String" + optional false + since "5.5.12" + } + column { + name "configuration" + enclosedIn "params" + desc "外部服务配置, 使用 json 格式" + location "body" + type "String" + optional false + since "5.5.12" + } + column { + name "description" + enclosedIn "params" + desc "资源的详细描述" + location "body" + type "String" + optional true + since "5.5.12" + } + column { + name "resourceUuid" + enclosedIn "params" + desc "资源UUID" + location "body" + type "String" + optional true + since "5.5.12" + } + column { + name "tagUuids" + enclosedIn "params" + desc "标签UUID列表" + location "body" + type "List" + optional true + since "5.5.12" + } + column { + name "systemTags" + enclosedIn "" + desc "系统标签" + location "body" + type "List" + optional true + since "5.5.12" + } + column { + name "userTags" + enclosedIn "" + desc "用户标签" + location "body" + type "List" + optional true + since "5.5.12" + } + } + } + + response { + clz APIAddExternalServiceConfigurationEvent.class + } + } +} \ No newline at end of file diff --git a/header/src/main/java/org/zstack/header/core/external/service/APIDeleteExternalServiceConfigurationEvent.java b/header/src/main/java/org/zstack/header/core/external/service/APIDeleteExternalServiceConfigurationEvent.java new file mode 100644 index 00000000000..d7ac0bf2f65 --- /dev/null +++ b/header/src/main/java/org/zstack/header/core/external/service/APIDeleteExternalServiceConfigurationEvent.java @@ -0,0 +1,21 @@ +package org.zstack.header.core.external.service; + +import org.zstack.header.message.APIEvent; +import org.zstack.header.rest.RestResponse; + +/** + * @Author: ya.wang + * @Date: 1/15/26 1:47 AM + */ +@RestResponse +public class APIDeleteExternalServiceConfigurationEvent extends APIEvent { + public APIDeleteExternalServiceConfigurationEvent() {} + + public APIDeleteExternalServiceConfigurationEvent(String apiId) { super(apiId); } + + public static APIDeleteExternalServiceConfigurationEvent __example__() { + APIDeleteExternalServiceConfigurationEvent event = new APIDeleteExternalServiceConfigurationEvent(); + event.setSuccess(true); + return event; + } +} diff --git a/header/src/main/java/org/zstack/header/core/external/service/APIDeleteExternalServiceConfigurationEventDoc_zh_cn.groovy b/header/src/main/java/org/zstack/header/core/external/service/APIDeleteExternalServiceConfigurationEventDoc_zh_cn.groovy new file mode 100644 index 00000000000..97e22e78bba --- /dev/null +++ b/header/src/main/java/org/zstack/header/core/external/service/APIDeleteExternalServiceConfigurationEventDoc_zh_cn.groovy @@ -0,0 +1,23 @@ +package org.zstack.header.core.external.service + +import org.zstack.header.errorcode.ErrorCode + +doc { + + title "删除外部服务配置返回" + + field { + name "success" + desc "操作是否成功" + type "boolean" + since "5.5.12" + } + ref { + name "error" + path "org.zstack.header.core.external.service.APIDeleteExternalServiceConfigurationEvent.error" + desc "错误码,若不为null,则表示操作失败, 操作成功时该字段为null",false + type "ErrorCode" + since "5.5.12" + clz ErrorCode.class + } +} diff --git a/header/src/main/java/org/zstack/header/core/external/service/APIDeleteExternalServiceConfigurationMsg.java b/header/src/main/java/org/zstack/header/core/external/service/APIDeleteExternalServiceConfigurationMsg.java new file mode 100644 index 00000000000..b04729437f2 --- /dev/null +++ b/header/src/main/java/org/zstack/header/core/external/service/APIDeleteExternalServiceConfigurationMsg.java @@ -0,0 +1,42 @@ +package org.zstack.header.core.external.service; + +import org.springframework.http.HttpMethod; +import org.zstack.header.message.APIDeleteMessage; +import org.zstack.header.message.APIEvent; +import org.zstack.header.message.APIMessage; +import org.zstack.header.message.APIParam; +import org.zstack.header.other.APIAuditor; +import org.zstack.header.rest.RestRequest; + +/** + * @Author: ya.wang + * @Date: 1/15/26 1:44 AM + */ +@RestRequest( + path = "/external/service/configuration/{uuid}", + responseClass = APIDeleteExternalServiceConfigurationEvent.class, + method = HttpMethod.DELETE +) +public class APIDeleteExternalServiceConfigurationMsg extends APIDeleteMessage implements APIAuditor { + @APIParam(resourceType = ExternalServiceConfigurationVO.class, successIfResourceNotExisting = true) + private String uuid; + + public String getUuid() { + return uuid; + } + + public void setUuid(String uuid) { + this.uuid = uuid; + } + + @Override + public Result audit(APIMessage msg, APIEvent rsp) { + return new APIAuditor.Result(((APIDeleteExternalServiceConfigurationMsg)msg).getUuid(), ExternalServiceConfigurationVO.class); + } + + public static APIDeleteExternalServiceConfigurationMsg __example__() { + APIDeleteExternalServiceConfigurationMsg msg = new APIDeleteExternalServiceConfigurationMsg(); + msg.setUuid(uuid()); + return msg; + } +} diff --git a/header/src/main/java/org/zstack/header/core/external/service/APIDeleteExternalServiceConfigurationMsgDoc_zh_cn.groovy b/header/src/main/java/org/zstack/header/core/external/service/APIDeleteExternalServiceConfigurationMsgDoc_zh_cn.groovy new file mode 100644 index 00000000000..3b9c86b5ba2 --- /dev/null +++ b/header/src/main/java/org/zstack/header/core/external/service/APIDeleteExternalServiceConfigurationMsgDoc_zh_cn.groovy @@ -0,0 +1,67 @@ +package org.zstack.header.core.external.service + +import org.zstack.header.core.external.service.APIDeleteExternalServiceConfigurationEvent + +doc { + title "删除外部服务配置" + + category "externalService" + + desc """删除外部服务配置""" + + rest { + request { + url "DELETE /v1/external/service/configuration/{uuid}" + + header (Authorization: 'OAuth the-session-uuid') + + clz APIDeleteExternalServiceConfigurationMsg.class + + desc """""" + + params { + + column { + name "uuid" + enclosedIn "" + desc "资源的UUID,唯一标示该资源" + location "url" + type "String" + optional false + since "5.5.12" + } + column { + name "deleteMode" + enclosedIn "" + desc "删除模式(Permissive / Enforcing,Permissive)" + location "body" + type "String" + optional true + since "5.5.12" + } + column { + name "systemTags" + enclosedIn "" + desc "系统标签" + location "body" + type "List" + optional true + since "5.5.12" + } + column { + name "userTags" + enclosedIn "" + desc "用户标签" + location "body" + type "List" + optional true + since "5.5.12" + } + } + } + + response { + clz APIDeleteExternalServiceConfigurationEvent.class + } + } +} \ No newline at end of file diff --git a/header/src/main/java/org/zstack/header/core/external/service/APIQueryExternalServiceConfigurationMsg.java b/header/src/main/java/org/zstack/header/core/external/service/APIQueryExternalServiceConfigurationMsg.java new file mode 100644 index 00000000000..1ec870e2b86 --- /dev/null +++ b/header/src/main/java/org/zstack/header/core/external/service/APIQueryExternalServiceConfigurationMsg.java @@ -0,0 +1,24 @@ +package org.zstack.header.core.external.service; + +import org.springframework.http.HttpMethod; +import org.zstack.header.query.APIQueryMessage; +import org.zstack.header.query.AutoQuery; +import org.zstack.header.rest.RestRequest; + +import java.util.Collections; +import java.util.List; + +/** + * @Author: ya.wang + * @Date: 1/15/26 1:36 AM + */ +@AutoQuery(replyClass = APIQueryExternalServiceConfigurationReply.class, inventoryClass = ExternalServiceConfigurationInventory.class) +@RestRequest( + path = "/external/service/configuration", + optionalPaths = {"/external/service/configuration/{uuid}"}, + method = HttpMethod.GET, + responseClass = APIQueryExternalServiceConfigurationReply.class +) +public class APIQueryExternalServiceConfigurationMsg extends APIQueryMessage { + public static List __example__() {return Collections.singletonList("uuid=" + uuid());} +} diff --git a/header/src/main/java/org/zstack/header/core/external/service/APIQueryExternalServiceConfigurationMsgDoc_zh_cn.groovy b/header/src/main/java/org/zstack/header/core/external/service/APIQueryExternalServiceConfigurationMsgDoc_zh_cn.groovy new file mode 100644 index 00000000000..b66929a1286 --- /dev/null +++ b/header/src/main/java/org/zstack/header/core/external/service/APIQueryExternalServiceConfigurationMsgDoc_zh_cn.groovy @@ -0,0 +1,31 @@ +package org.zstack.header.core.external.service + +import org.zstack.header.core.external.service.APIQueryExternalServiceConfigurationReply +import org.zstack.header.query.APIQueryMessage + +doc { + title "查询外部服务配置" + + category "externalService" + + desc """查询外部服务配置""" + + rest { + request { + url "GET /v1/external/service/configuration" + url "GET /v1/external/service/configuration/{uuid}" + + header (Authorization: 'OAuth the-session-uuid') + + clz APIQueryExternalServiceConfigurationMsg.class + + desc """""" + + params APIQueryMessage.class + } + + response { + clz APIQueryExternalServiceConfigurationReply.class + } + } +} \ No newline at end of file diff --git a/header/src/main/java/org/zstack/header/core/external/service/APIQueryExternalServiceConfigurationReply.java b/header/src/main/java/org/zstack/header/core/external/service/APIQueryExternalServiceConfigurationReply.java new file mode 100644 index 00000000000..87456312eeb --- /dev/null +++ b/header/src/main/java/org/zstack/header/core/external/service/APIQueryExternalServiceConfigurationReply.java @@ -0,0 +1,32 @@ +package org.zstack.header.core.external.service; + +import org.zstack.header.query.APIQueryReply; +import org.zstack.header.rest.RestResponse; + +import java.util.List; + +import static org.zstack.utils.CollectionDSL.list; + +/** + * @Author: ya.wang + * @Date: 1/15/26 1:39 AM + */ +@RestResponse(allTo = "inventories") +public class APIQueryExternalServiceConfigurationReply extends APIQueryReply { + private List inventories; + + public List getInventories() {return inventories;} + + public void setInventories(List inventories) {this.inventories = inventories;} + + public static APIQueryExternalServiceConfigurationReply __example__() { + APIQueryExternalServiceConfigurationReply reply = new APIQueryExternalServiceConfigurationReply(); + ExternalServiceConfigurationInventory inv = new ExternalServiceConfigurationInventory(); + + inv.setUuid(uuid()); + inv.setServiceType("Prometheus2"); + inv.setConfiguration("{}"); + reply.setInventories(list(inv)); + return reply; + } +} diff --git a/header/src/main/java/org/zstack/header/core/external/service/APIQueryExternalServiceConfigurationReplyDoc_zh_cn.groovy b/header/src/main/java/org/zstack/header/core/external/service/APIQueryExternalServiceConfigurationReplyDoc_zh_cn.groovy new file mode 100644 index 00000000000..dfeb4e14316 --- /dev/null +++ b/header/src/main/java/org/zstack/header/core/external/service/APIQueryExternalServiceConfigurationReplyDoc_zh_cn.groovy @@ -0,0 +1,32 @@ +package org.zstack.header.core.external.service + +import org.zstack.header.core.external.service.ExternalServiceConfigurationInventory +import org.zstack.header.errorcode.ErrorCode + +doc { + + title "外部服务配置清单" + + ref { + name "inventories" + path "org.zstack.header.core.external.service.APIQueryExternalServiceConfigurationReply.inventories" + desc "null" + type "List" + since "5.5.12" + clz ExternalServiceConfigurationInventory.class + } + field { + name "success" + desc "" + type "boolean" + since "5.5.12" + } + ref { + name "error" + path "org.zstack.header.core.external.service.APIQueryExternalServiceConfigurationReply.error" + desc "错误码,若不为null,则表示操作失败, 操作成功时该字段为null",false + type "ErrorCode" + since "5.5.12" + clz ErrorCode.class + } +} diff --git a/header/src/main/java/org/zstack/header/core/external/service/APIUpdateExternalServiceConfigurationEvent.java b/header/src/main/java/org/zstack/header/core/external/service/APIUpdateExternalServiceConfigurationEvent.java new file mode 100644 index 00000000000..c7e5dae73b5 --- /dev/null +++ b/header/src/main/java/org/zstack/header/core/external/service/APIUpdateExternalServiceConfigurationEvent.java @@ -0,0 +1,30 @@ +package org.zstack.header.core.external.service; + +import org.zstack.header.message.APIEvent; +import org.zstack.header.rest.RestResponse; + +/** + * @Author: ya.wang + * @Date: 1/15/26 1:53 AM + */ +@RestResponse(allTo = "inventory") +public class APIUpdateExternalServiceConfigurationEvent extends APIEvent { + private ExternalServiceConfigurationInventory inventory; + + public APIUpdateExternalServiceConfigurationEvent() {} + + public APIUpdateExternalServiceConfigurationEvent(String apiId) { super(apiId); } + + public ExternalServiceConfigurationInventory getInventory() {return inventory;} + + public void setInventory(ExternalServiceConfigurationInventory inventory) {this.inventory = inventory;} + + public static APIUpdateExternalServiceConfigurationEvent __example__() { + APIUpdateExternalServiceConfigurationEvent event = new APIUpdateExternalServiceConfigurationEvent(); + ExternalServiceConfigurationInventory inv = new ExternalServiceConfigurationInventory(); + + inv.setUuid(uuid()); + event.setInventory(inv); + return event; + } +} diff --git a/header/src/main/java/org/zstack/header/core/external/service/APIUpdateExternalServiceConfigurationEventDoc_zh_cn.groovy b/header/src/main/java/org/zstack/header/core/external/service/APIUpdateExternalServiceConfigurationEventDoc_zh_cn.groovy new file mode 100644 index 00000000000..43ef3bfc0fa --- /dev/null +++ b/header/src/main/java/org/zstack/header/core/external/service/APIUpdateExternalServiceConfigurationEventDoc_zh_cn.groovy @@ -0,0 +1,32 @@ +package org.zstack.header.core.external.service + +import org.zstack.header.core.external.service.ExternalServiceConfigurationInventory +import org.zstack.header.errorcode.ErrorCode + +doc { + + title "更新外部服务配置" + + ref { + name "inventory" + path "org.zstack.header.core.external.service.APIUpdateExternalServiceConfigurationEvent.inventory" + desc "null" + type "ExternalServiceConfigurationInventory" + since "5.5.12" + clz ExternalServiceConfigurationInventory.class + } + field { + name "success" + desc "" + type "boolean" + since "5.5.12" + } + ref { + name "error" + path "org.zstack.header.core.external.service.APIUpdateExternalServiceConfigurationEvent.error" + desc "错误码,若不为null,则表示操作失败, 操作成功时该字段为null",false + type "ErrorCode" + since "5.5.12" + clz ErrorCode.class + } +} diff --git a/header/src/main/java/org/zstack/header/core/external/service/APIUpdateExternalServiceConfigurationMsg.java b/header/src/main/java/org/zstack/header/core/external/service/APIUpdateExternalServiceConfigurationMsg.java new file mode 100644 index 00000000000..1fd29bd06b4 --- /dev/null +++ b/header/src/main/java/org/zstack/header/core/external/service/APIUpdateExternalServiceConfigurationMsg.java @@ -0,0 +1,54 @@ +package org.zstack.header.core.external.service; + +import org.springframework.http.HttpMethod; +import org.zstack.header.message.APIEvent; +import org.zstack.header.message.APIMessage; +import org.zstack.header.message.APIParam; +import org.zstack.header.other.APIAuditor; +import org.zstack.header.rest.RestRequest; + +/** + * @Author: ya.wang + * @Date: 1/15/26 1:49 AM + */ +@RestRequest( + path = "/external/service/configuration/{uuid}", + isAction = true, + method = HttpMethod.PUT, + responseClass = APIUpdateExternalServiceConfigurationEvent.class +) +public class APIUpdateExternalServiceConfigurationMsg extends APIMessage implements APIAuditor { + @APIParam(resourceType = ExternalServiceConfigurationVO.class, maxLength = 32, operationTarget = true) + private String uuid; + + @APIParam(maxLength = 2048, required = false) + private String description; + + public String getUuid() { + return uuid; + } + + public void setUuid(String uuid) { + this.uuid = uuid; + } + + public String getDescription() { + return description; + } + + public void setDescription(String description) { + this.description = description; + } + + @Override + public Result audit(APIMessage msg, APIEvent rsp) { + return new APIAuditor.Result(((APIUpdateExternalServiceConfigurationMsg)msg).getUuid(), ExternalServiceConfigurationVO.class); + } + + public static APIUpdateExternalServiceConfigurationMsg __example__() { + APIUpdateExternalServiceConfigurationMsg msg = new APIUpdateExternalServiceConfigurationMsg(); + msg.setUuid(uuid()); + msg.setDescription("description"); + return msg; + } +} diff --git a/header/src/main/java/org/zstack/header/core/external/service/APIUpdateExternalServiceConfigurationMsgDoc_zh_cn.groovy b/header/src/main/java/org/zstack/header/core/external/service/APIUpdateExternalServiceConfigurationMsgDoc_zh_cn.groovy new file mode 100644 index 00000000000..b38094f7cb1 --- /dev/null +++ b/header/src/main/java/org/zstack/header/core/external/service/APIUpdateExternalServiceConfigurationMsgDoc_zh_cn.groovy @@ -0,0 +1,67 @@ +package org.zstack.header.core.external.service + +import org.zstack.header.core.external.service.APIUpdateExternalServiceConfigurationEvent + +doc { + title "UpdateExternalServiceConfiguration" + + category "externalService" + + desc """在这里填写API描述""" + + rest { + request { + url "PUT /v1/external/service/configuration/{uuid}" + + header (Authorization: 'OAuth the-session-uuid') + + clz APIUpdateExternalServiceConfigurationMsg.class + + desc """""" + + params { + + column { + name "uuid" + enclosedIn "updateExternalServiceConfiguration" + desc "资源的UUID,唯一标示该资源" + location "url" + type "String" + optional false + since "5.5.12" + } + column { + name "description" + enclosedIn "updateExternalServiceConfiguration" + desc "资源的详细描述" + location "body" + type "String" + optional true + since "5.5.12" + } + column { + name "systemTags" + enclosedIn "" + desc "系统标签" + location "body" + type "List" + optional true + since "5.5.12" + } + column { + name "userTags" + enclosedIn "" + desc "用户标签" + location "body" + type "List" + optional true + since "5.5.12" + } + } + } + + response { + clz APIUpdateExternalServiceConfigurationEvent.class + } + } +} \ No newline at end of file diff --git a/header/src/main/java/org/zstack/header/core/external/service/ApplyExternalConfigurationResult.java b/header/src/main/java/org/zstack/header/core/external/service/ApplyExternalConfigurationResult.java new file mode 100644 index 00000000000..66a75a20cc9 --- /dev/null +++ b/header/src/main/java/org/zstack/header/core/external/service/ApplyExternalConfigurationResult.java @@ -0,0 +1,39 @@ +package org.zstack.header.core.external.service; + +import org.zstack.header.errorcode.ErrorCode; + +/** + * @Author: ya.wang + * @Date: 1/15/26 2:50 AM + */ +public class ApplyExternalConfigurationResult { + + private String managementNodeUuid; + private ErrorCode errorCode; + private boolean success = true; + + public String getManagementNodeUuid() { + return managementNodeUuid; + } + + public void setManagementNodeUuid(String managementNodeUuid) { + this.managementNodeUuid = managementNodeUuid; + } + + public boolean isSuccess() { + return success; + } + + public void setSuccess(boolean success) { + this.success = success; + } + + public ErrorCode getErrorCode() { + return errorCode; + } + + public void setErrorCode(ErrorCode errorCode) { + this.success = false; + this.errorCode = errorCode; + } +} diff --git a/header/src/main/java/org/zstack/header/core/external/service/ApplyExternalServiceConfigurationMsg.java b/header/src/main/java/org/zstack/header/core/external/service/ApplyExternalServiceConfigurationMsg.java new file mode 100644 index 00000000000..dd82db96f72 --- /dev/null +++ b/header/src/main/java/org/zstack/header/core/external/service/ApplyExternalServiceConfigurationMsg.java @@ -0,0 +1,19 @@ +package org.zstack.header.core.external.service; + +import org.zstack.header.message.NeedReplyMessage; + +/** + * @Author: ya.wang + * @Date: 1/15/26 2:59 AM + */ +public class ApplyExternalServiceConfigurationMsg extends NeedReplyMessage { + private String serviceType; + + public String getServiceType() { + return serviceType; + } + + public void setServiceType(String serviceType) { + this.serviceType = serviceType; + } +} diff --git a/header/src/main/java/org/zstack/header/core/external/service/ApplyExternalServiceConfigurationReply.java b/header/src/main/java/org/zstack/header/core/external/service/ApplyExternalServiceConfigurationReply.java new file mode 100644 index 00000000000..08c25e04266 --- /dev/null +++ b/header/src/main/java/org/zstack/header/core/external/service/ApplyExternalServiceConfigurationReply.java @@ -0,0 +1,28 @@ +package org.zstack.header.core.external.service; + +import org.zstack.header.message.MessageReply; + +/** + * @Author: ya.wang + * @Date: 1/15/26 3:26 AM + */ +public class ApplyExternalServiceConfigurationReply extends MessageReply { + private String managementNodeUuid; + private String value; + + public String getManagementNodeUuid() { + return managementNodeUuid; + } + + public void setManagementNodeUuid(String managementNodeUuid) { + this.managementNodeUuid = managementNodeUuid; + } + + public String getValue() { + return value; + } + + public void setValue(String value) { + this.value = value; + } +} diff --git a/header/src/main/java/org/zstack/header/core/external/service/ExternalServiceConfigurationInventory.java b/header/src/main/java/org/zstack/header/core/external/service/ExternalServiceConfigurationInventory.java new file mode 100644 index 00000000000..2ed907dc61d --- /dev/null +++ b/header/src/main/java/org/zstack/header/core/external/service/ExternalServiceConfigurationInventory.java @@ -0,0 +1,89 @@ +package org.zstack.header.core.external.service; + +import org.zstack.header.search.Inventory; + +import java.sql.Timestamp; +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; + +/** + * @Author: ya.wang + * @Date: 1/15/26 1:31 AM + */ +@Inventory(mappingVOClass = ExternalServiceConfigurationVO.class) +public class ExternalServiceConfigurationInventory { + private String uuid; + private String serviceType; + private String configuration; + private String description; + private Timestamp createDate; + private Timestamp lastOpDate; + + public static ExternalServiceConfigurationInventory valueOf(ExternalServiceConfigurationVO vo) { + ExternalServiceConfigurationInventory inv = new ExternalServiceConfigurationInventory(); + inv.setUuid(vo.getUuid()); + inv.setDescription(vo.getDescription()); + inv.setServiceType(vo.getServiceType()); + inv.setConfiguration(vo.getConfiguration()); + inv.setCreateDate(vo.getCreateDate()); + inv.setLastOpDate(vo.getLastOpDate()); + return inv; + } + + public static List valueOf(Collection vos) { + List invs = new ArrayList(); + for (ExternalServiceConfigurationVO vo : vos) { + invs.add(valueOf(vo)); + } + return invs; + } + + public Timestamp getLastOpDate() { + return lastOpDate; + } + + public void setLastOpDate(Timestamp lastOpDate) { + this.lastOpDate = lastOpDate; + } + + public Timestamp getCreateDate() { + return createDate; + } + + public void setCreateDate(Timestamp createDate) { + this.createDate = createDate; + } + + public String getDescription() { + return description; + } + + public void setDescription(String description) { + this.description = description; + } + + public String getConfiguration() { + return configuration; + } + + public void setConfiguration(String configuration) { + this.configuration = configuration; + } + + public String getServiceType() { + return serviceType; + } + + public void setServiceType(String serviceType) { + this.serviceType = serviceType; + } + + public String getUuid() { + return uuid; + } + + public void setUuid(String uuid) { + this.uuid = uuid; + } +} diff --git a/header/src/main/java/org/zstack/header/core/external/service/ExternalServiceConfigurationInventoryDoc_zh_cn.groovy b/header/src/main/java/org/zstack/header/core/external/service/ExternalServiceConfigurationInventoryDoc_zh_cn.groovy new file mode 100644 index 00000000000..008bd93c094 --- /dev/null +++ b/header/src/main/java/org/zstack/header/core/external/service/ExternalServiceConfigurationInventoryDoc_zh_cn.groovy @@ -0,0 +1,45 @@ +package org.zstack.header.core.external.service + +import java.sql.Timestamp + +doc { + + title "外部服务配置" + + field { + name "uuid" + desc "资源的UUID,唯一标示该资源" + type "String" + since "5.5.12" + } + field { + name "serviceType" + desc "外部服务类型, 如 Prometheus2, FluentBitServer" + type "String" + since "5.5.12" + } + field { + name "configuration" + desc "外部服务配置, 使用 json 格式" + type "String" + since "5.5.12" + } + field { + name "description" + desc "资源的详细描述" + type "String" + since "5.5.12" + } + field { + name "createDate" + desc "创建时间" + type "Timestamp" + since "5.5.12" + } + field { + name "lastOpDate" + desc "最后一次修改时间" + type "Timestamp" + since "5.5.12" + } +} diff --git a/header/src/main/java/org/zstack/header/core/external/service/ExternalServiceConfigurationVO.java b/header/src/main/java/org/zstack/header/core/external/service/ExternalServiceConfigurationVO.java new file mode 100644 index 00000000000..86f6ade106d --- /dev/null +++ b/header/src/main/java/org/zstack/header/core/external/service/ExternalServiceConfigurationVO.java @@ -0,0 +1,72 @@ +package org.zstack.header.core.external.service; + +import org.zstack.header.vo.ResourceVO; +import org.zstack.header.vo.ToInventory; + +import javax.persistence.Column; +import javax.persistence.Entity; +import javax.persistence.PreUpdate; +import javax.persistence.Table; +import java.sql.Timestamp; + +/** + * @Author: ya.wang + * @Date: 1/15/26 1:25 AM + */ +@Entity +@Table +public class ExternalServiceConfigurationVO extends ResourceVO implements ToInventory { + @Column + private String serviceType; + @Column + private String configuration; + @Column + private String description; + @Column + private Timestamp createDate; + @Column + private Timestamp lastOpDate; + + public String getServiceType() { + return serviceType; + } + + @PreUpdate + private void preUpdate() { lastOpDate = null; } + + public void setServiceType(String serviceType) { + this.serviceType = serviceType; + } + + public String getConfiguration() { + return configuration; + } + + public void setConfiguration(String configuration) { + this.configuration = configuration; + } + + public String getDescription() { + return description; + } + + public void setDescription(String description) { + this.description = description; + } + + public Timestamp getCreateDate() { + return createDate; + } + + public void setCreateDate(Timestamp createDate) { + this.createDate = createDate; + } + + public Timestamp getLastOpDate() { + return lastOpDate; + } + + public void setLastOpDate(Timestamp lastOpDate) { + this.lastOpDate = lastOpDate; + } +} diff --git a/header/src/main/java/org/zstack/header/core/external/service/ExternalServiceConfigurationVO_.java b/header/src/main/java/org/zstack/header/core/external/service/ExternalServiceConfigurationVO_.java new file mode 100644 index 00000000000..6636e32b1c9 --- /dev/null +++ b/header/src/main/java/org/zstack/header/core/external/service/ExternalServiceConfigurationVO_.java @@ -0,0 +1,18 @@ +package org.zstack.header.core.external.service; + +import org.zstack.header.vo.ResourceVO_; + +import javax.persistence.metamodel.SingularAttribute; +import java.sql.Timestamp; + +/** + * @Author: ya.wang + * @Date: 1/15/26 1:30 AM + */ +public class ExternalServiceConfigurationVO_ extends ResourceVO_ { + public static volatile SingularAttribute serviceType; + public static volatile SingularAttribute configuration; + public static volatile SingularAttribute description; + public static volatile SingularAttribute createDate; + public static volatile SingularAttribute lastOpDate; +} diff --git a/header/src/main/java/org/zstack/header/core/external/service/ExternalServiceInventory.java b/header/src/main/java/org/zstack/header/core/external/service/ExternalServiceInventory.java index e1b14c73baa..bddfad86a84 100644 --- a/header/src/main/java/org/zstack/header/core/external/service/ExternalServiceInventory.java +++ b/header/src/main/java/org/zstack/header/core/external/service/ExternalServiceInventory.java @@ -4,6 +4,7 @@ public class ExternalServiceInventory { private String name; private String status; private ExternalServiceCapabilities capabilities; + private String serviceType; public String getName() { return name; @@ -29,6 +30,14 @@ public void setCapabilities(ExternalServiceCapabilities capabilities) { this.capabilities = capabilities; } + public String getServiceType() { + return serviceType; + } + + public void setServiceType(String serviceType) { + this.serviceType = serviceType; + } + public static ExternalServiceInventory __example__() { ExternalServiceInventory inv = new ExternalServiceInventory(); inv.setName("prometheus"); @@ -36,6 +45,7 @@ public static ExternalServiceInventory __example__() { ExternalServiceCapabilities cap = new ExternalServiceCapabilities(); cap.setReloadConfig(true); inv.setCapabilities(cap); + inv.setServiceType("Prometheus2"); return inv; } } diff --git a/header/src/main/java/org/zstack/header/core/external/service/RBACInfo.java b/header/src/main/java/org/zstack/header/core/external/service/RBACInfo.java index 21b5c6b03e0..4537d62ee06 100644 --- a/header/src/main/java/org/zstack/header/core/external/service/RBACInfo.java +++ b/header/src/main/java/org/zstack/header/core/external/service/RBACInfo.java @@ -9,7 +9,11 @@ public void permissions() { permissionBuilder() .adminOnlyAPIs( APIGetExternalServicesMsg.class, - APIReloadExternalServiceMsg.class + APIReloadExternalServiceMsg.class, + APIAddExternalServiceConfigurationMsg.class, + APIQueryExternalServiceConfigurationMsg.class, + APIUpdateExternalServiceConfigurationMsg.class, + APIDeleteExternalServiceConfigurationMsg.class ).build(); } diff --git a/header/src/main/java/org/zstack/header/core/progress/APIGetTaskProgressReply.java b/header/src/main/java/org/zstack/header/core/progress/APIGetTaskProgressReply.java index 44e27d0b8e3..3479c83e909 100755 --- a/header/src/main/java/org/zstack/header/core/progress/APIGetTaskProgressReply.java +++ b/header/src/main/java/org/zstack/header/core/progress/APIGetTaskProgressReply.java @@ -6,6 +6,7 @@ import java.util.List; import static java.util.Arrays.asList; + /** * Created by xing5 on 2017/3/21. */ @@ -29,6 +30,18 @@ public static APIGetTaskProgressReply __example__() { inv.setTaskUuid("931102503f64436ea649939ff3957406"); inv.setTime(DocUtils.date); inv.setType("Task"); + + LongJobProgressDetail detail = new LongJobProgressDetail(); + detail.setPercent(42); + detail.setStage("downloading"); + detail.setState("running"); + detail.setProcessed(440401920L); + detail.setTotal(1073741824L); + detail.setUnit("bytes"); + detail.setSpeed(10485760L); + detail.setEstimatedRemainingSeconds(60L); + inv.setProgressDetail(detail); + msg.setInventories(asList(inv)); return msg; } diff --git a/header/src/main/java/org/zstack/header/core/progress/LongJobProgressDetail.java b/header/src/main/java/org/zstack/header/core/progress/LongJobProgressDetail.java new file mode 100644 index 00000000000..d4d5e1d56bd --- /dev/null +++ b/header/src/main/java/org/zstack/header/core/progress/LongJobProgressDetail.java @@ -0,0 +1,146 @@ +package org.zstack.header.core.progress; + +import java.util.Map; + +/** + * Standardized LongJob progress detail, parsed from TaskProgressVO.opaque. + * + * All fields are optional (nullable). Callers should null-check before use. + * This is a pure read-only view — the database schema (TaskProgressVO) is unchanged. + */ +public class LongJobProgressDetail { + /** Progress percentage 0-100, if known. */ + private Integer percent; + + /** Human-readable stage label, e.g. "downloading", "extracting". */ + private String stage; + + /** State identifier, e.g. "running", "paused". */ + private String state; + + /** Human-readable reason for current state. */ + private String stateReason; + + /** Amount already processed (unit described by the {@code unit} field). */ + private Long processed; + + /** Total amount to process (unit described by the {@code unit} field). */ + private Long total; + + /** Items already processed (e.g. files, chunks). */ + private Long processedItems; + + /** Total items to process. */ + private Long totalItems; + + /** Processing speed per second (unit described by the {@code unit} field). */ + private Long speed; + + /** Unit for processed/total/speed, e.g. "bytes", "items", "steps". */ + private String unit; + + /** Estimated remaining time in seconds. */ + private Long estimatedRemainingSeconds; + + /** + * Catch-all for any opaque fields that don't map to the standard schema. + * Preserves unknown keys so no data is silently dropped. + */ + private Map extra; + + public Integer getPercent() { + return percent; + } + + public void setPercent(Integer percent) { + this.percent = percent; + } + + public String getStage() { + return stage; + } + + public void setStage(String stage) { + this.stage = stage; + } + + public String getState() { + return state; + } + + public void setState(String state) { + this.state = state; + } + + public String getStateReason() { + return stateReason; + } + + public void setStateReason(String stateReason) { + this.stateReason = stateReason; + } + + public Long getProcessed() { + return processed; + } + + public void setProcessed(Long processed) { + this.processed = processed; + } + + public Long getTotal() { + return total; + } + + public void setTotal(Long total) { + this.total = total; + } + + public Long getProcessedItems() { + return processedItems; + } + + public void setProcessedItems(Long processedItems) { + this.processedItems = processedItems; + } + + public Long getTotalItems() { + return totalItems; + } + + public void setTotalItems(Long totalItems) { + this.totalItems = totalItems; + } + + public Long getSpeed() { + return speed; + } + + public void setSpeed(Long speed) { + this.speed = speed; + } + + public String getUnit() { + return unit; + } + + public void setUnit(String unit) { + this.unit = unit; + } + + public Long getEstimatedRemainingSeconds() { + return estimatedRemainingSeconds; + } + + public void setEstimatedRemainingSeconds(Long estimatedRemainingSeconds) { + this.estimatedRemainingSeconds = estimatedRemainingSeconds; + } + + public Map getExtra() { + return extra; + } + + public void setExtra(Map extra) { + this.extra = extra; + } +} diff --git a/header/src/main/java/org/zstack/header/core/progress/LongJobProgressDetailBuilder.java b/header/src/main/java/org/zstack/header/core/progress/LongJobProgressDetailBuilder.java new file mode 100644 index 00000000000..3de92b7c473 --- /dev/null +++ b/header/src/main/java/org/zstack/header/core/progress/LongJobProgressDetailBuilder.java @@ -0,0 +1,142 @@ +package org.zstack.header.core.progress; + +import org.zstack.utils.Utils; +import org.zstack.utils.gson.JSONObjectUtil; +import org.zstack.utils.logging.CLogger; + +import java.util.HashMap; +import java.util.Map; + +/** + * Parses TaskProgressVO.opaque (free-form JSON) into a typed LongJobProgressDetail. + * + * All agents now send a standardized camelCase format: + * {"processed":N, "total":N, "percent":N, "stage":"migrating", + * "speed":N, "estimatedRemainingSeconds":N, "state":"running", + * "stateReason":"...", "processedItems":N, "totalItems":N, "unit":"bytes"} + * + * The "unit" field tells the UI how to format processed/total/speed: + * "bytes" — byte quantities (format as KB/MB/GB) + * "items" — discrete items (format as count) + * "steps" — workflow steps + * + * Unknown keys are preserved in LongJobProgressDetail.extra so no data is silently dropped. + */ +public class LongJobProgressDetailBuilder { + private static final CLogger logger = Utils.getLogger(LongJobProgressDetailBuilder.class); + + private static final String[] KNOWN_KEYS = { + "processed", "total", "percent", "stage", "state", "stateReason", + "speed", "estimatedRemainingSeconds", "processedItems", "totalItems", "unit" + }; + + private LongJobProgressDetailBuilder() {} + + /** + * Build a LongJobProgressDetail from a TaskProgressVO. + * Returns null if opaque is null/empty or not valid JSON. + */ + public static LongJobProgressDetail fromTaskProgressVO(TaskProgressVO vo) { + if (vo == null || vo.getOpaque() == null || vo.getOpaque().isEmpty()) { + return null; + } + + Map raw; + try { + raw = JSONObjectUtil.toObject(vo.getOpaque(), HashMap.class); + } catch (Exception e) { + logger.trace("LongJobProgressDetailBuilder: opaque is not a JSON object, skipping: " + vo.getOpaque(), e); + return null; + } + + if (raw == null || raw.isEmpty()) { + return null; + } + + try { + LongJobProgressDetail detail = new LongJobProgressDetail(); + + Number processed = toNumber(raw.get("processed")); + if (processed != null) { + detail.setProcessed(processed.longValue()); + } + + Number total = toNumber(raw.get("total")); + if (total != null) { + detail.setTotal(total.longValue()); + } + + Number percent = toNumber(raw.get("percent")); + if (percent != null) { + detail.setPercent(Math.max(0, Math.min(100, (int) Math.round(percent.doubleValue())))); + } + + Object stage = raw.get("stage"); + if (stage instanceof String) { + detail.setStage((String) stage); + } + + Object state = raw.get("state"); + if (state instanceof String) { + detail.setState((String) state); + } + + Object stateReason = raw.get("stateReason"); + if (stateReason instanceof String) { + detail.setStateReason((String) stateReason); + } + + Number speed = toNumber(raw.get("speed")); + if (speed != null) { + detail.setSpeed(speed.longValue()); + } + + Number eta = toNumber(raw.get("estimatedRemainingSeconds")); + if (eta != null) { + detail.setEstimatedRemainingSeconds(eta.longValue()); + } + + Number processedItems = toNumber(raw.get("processedItems")); + if (processedItems != null) { + detail.setProcessedItems(processedItems.longValue()); + } + + Number totalItems = toNumber(raw.get("totalItems")); + if (totalItems != null) { + detail.setTotalItems(totalItems.longValue()); + } + + Object unit = raw.get("unit"); + if (unit instanceof String) { + detail.setUnit((String) unit); + } + + // Carry over any unrecognized keys into extra + Map extra = new HashMap<>(raw); + for (String key : KNOWN_KEYS) { + extra.remove(key); + } + if (!extra.isEmpty()) { + detail.setExtra(extra); + } + + return detail; + } catch (Exception e) { + logger.trace("LongJobProgressDetailBuilder: failed to parse standard format", e); + return null; + } + } + + private static Number toNumber(Object val) { + if (val instanceof Number) { + return (Number) val; + } + if (val instanceof String) { + try { + return Double.parseDouble((String) val); + } catch (NumberFormatException ignored) { + } + } + return null; + } +} diff --git a/header/src/main/java/org/zstack/header/core/progress/TaskProgressInventory.java b/header/src/main/java/org/zstack/header/core/progress/TaskProgressInventory.java index aa8c0f6db7e..b945d270292 100755 --- a/header/src/main/java/org/zstack/header/core/progress/TaskProgressInventory.java +++ b/header/src/main/java/org/zstack/header/core/progress/TaskProgressInventory.java @@ -18,6 +18,8 @@ public class TaskProgressInventory { private Long time; private List subTasks; private String arguments; + /** Typed progress detail parsed from opaque. Null when opaque is absent or parsing fails. */ + private LongJobProgressDetail progressDetail; public TaskProgressInventory() { } @@ -105,4 +107,12 @@ public Long getTime() { public void setTime(Long time) { this.time = time; } + + public LongJobProgressDetail getProgressDetail() { + return progressDetail; + } + + public void setProgressDetail(LongJobProgressDetail progressDetail) { + this.progressDetail = progressDetail; + } } diff --git a/header/src/main/java/org/zstack/header/errorcode/ErrorCode.java b/header/src/main/java/org/zstack/header/errorcode/ErrorCode.java index c7ff98024b3..d180b6b4ccb 100755 --- a/header/src/main/java/org/zstack/header/errorcode/ErrorCode.java +++ b/header/src/main/java/org/zstack/header/errorcode/ErrorCode.java @@ -104,6 +104,8 @@ public ErrorCode(ErrorCode other) { this.message = other.message; this.formatArgs = other.formatArgs == null ? null : other.formatArgs.clone(); this.globalErrorCode = other.globalErrorCode; + this.cost = other.cost; + this.opaque = other.opaque; } public void setCode(String code) { diff --git a/header/src/main/java/org/zstack/header/identity/RBACInfo.java b/header/src/main/java/org/zstack/header/identity/RBACInfo.java index 609a916d20c..81223064cd9 100755 --- a/header/src/main/java/org/zstack/header/identity/RBACInfo.java +++ b/header/src/main/java/org/zstack/header/identity/RBACInfo.java @@ -12,6 +12,7 @@ public void permissions() { .name("identity") .adminOnlyAPIs( APICreateAccountMsg.class, + APIQueryAccountMsg.class, APIShareResourceMsg.class, APIRevokeResourceSharingMsg.class, APIUpdateQuotaMsg.class, diff --git a/header/src/main/java/org/zstack/header/longjob/LongJobProgressNotificationMessage.java b/header/src/main/java/org/zstack/header/longjob/LongJobProgressNotificationMessage.java index 802f3b2b3fa..84b9aaf6ef9 100644 --- a/header/src/main/java/org/zstack/header/longjob/LongJobProgressNotificationMessage.java +++ b/header/src/main/java/org/zstack/header/longjob/LongJobProgressNotificationMessage.java @@ -1,5 +1,7 @@ package org.zstack.header.longjob; +import org.zstack.header.core.progress.LongJobProgressDetail; +import org.zstack.header.core.progress.LongJobProgressDetailBuilder; import org.zstack.header.core.progress.TaskProgressInventory; import org.zstack.header.core.progress.TaskProgressVO; @@ -23,6 +25,8 @@ public enum EventType { private Integer progress; /** Full progress detail; optional, BFF current version only needs {@link #getProgress()}. */ private TaskProgressInventory taskProgress; + /** Standardized progress detail parsed from opaque; null when opaque is absent. */ + private LongJobProgressDetail progressDetail; private EventType eventType; private Long timestamp; @@ -68,6 +72,14 @@ public void setTimestamp(Long timestamp) { this.timestamp = timestamp; } + public LongJobProgressDetail getProgressDetail() { + return progressDetail; + } + + public void setProgressDetail(LongJobProgressDetail progressDetail) { + this.progressDetail = progressDetail; + } + public static LongJobProgressNotificationMessage stateChanged(LongJobVO vo) { LongJobProgressNotificationMessage msg = new LongJobProgressNotificationMessage(); msg.longJob = LongJobInventory.valueOf(vo); @@ -85,7 +97,9 @@ public static LongJobProgressNotificationMessage progressUpdated(LongJobVO vo, T if (progressVO.getContent() != null) { inv.setContent(progressVO.getContent()); } + inv.setProgressDetail(LongJobProgressDetailBuilder.fromTaskProgressVO(progressVO)); msg.taskProgress = inv; + msg.progressDetail = inv.getProgressDetail(); msg.eventType = EventType.PROGRESS_UPDATED; msg.timestamp = System.currentTimeMillis(); return msg; diff --git a/header/src/main/java/org/zstack/header/network/l3/AllocateIpMsg.java b/header/src/main/java/org/zstack/header/network/l3/AllocateIpMsg.java index 849ebe4c96c..78af601bea6 100755 --- a/header/src/main/java/org/zstack/header/network/l3/AllocateIpMsg.java +++ b/header/src/main/java/org/zstack/header/network/l3/AllocateIpMsg.java @@ -12,6 +12,10 @@ public class AllocateIpMsg extends NeedReplyMessage implements L3NetworkMessage, private String ipRangeUuid; private String ipRangeType; private int ipVersion = IPv6Constants.IPv4; + private String netmask; + private String gateway; + private String ipv6Gateway; + private String ipv6Prefix; public String getRequiredIp() { return requiredIp; @@ -74,4 +78,40 @@ public int getIpVersion() { public void setIpVersion(int ipVersion) { this.ipVersion = ipVersion; } + + @Override + public String getNetmask() { + return netmask; + } + + public void setNetmask(String netmask) { + this.netmask = netmask; + } + + @Override + public String getGateway() { + return gateway; + } + + public void setGateway(String gateway) { + this.gateway = gateway; + } + + @Override + public String getIpv6Gateway() { + return ipv6Gateway; + } + + public void setIpv6Gateway(String ipv6Gateway) { + this.ipv6Gateway = ipv6Gateway; + } + + @Override + public String getIpv6Prefix() { + return ipv6Prefix; + } + + public void setIpv6Prefix(String ipv6Prefix) { + this.ipv6Prefix = ipv6Prefix; + } } diff --git a/header/src/main/java/org/zstack/header/network/l3/IpAllocateMessage.java b/header/src/main/java/org/zstack/header/network/l3/IpAllocateMessage.java index 4bb49d9fc8f..0d886bab7b5 100755 --- a/header/src/main/java/org/zstack/header/network/l3/IpAllocateMessage.java +++ b/header/src/main/java/org/zstack/header/network/l3/IpAllocateMessage.java @@ -15,6 +15,22 @@ default String getExcludedIp() { default boolean isDuplicatedIpAllowed() {return false;} + default String getNetmask() { + return null; + } + + default String getGateway() { + return null; + } + + default String getIpv6Gateway() { + return null; + } + + default String getIpv6Prefix() { + return null; + } + void setIpRangeUuid(String ipRangeUuid); void setRequiredIp(String requiredIp); diff --git a/header/src/main/java/org/zstack/header/network/l3/UsedIpInventory.java b/header/src/main/java/org/zstack/header/network/l3/UsedIpInventory.java index 775dc66d9c5..c9f406ae985 100755 --- a/header/src/main/java/org/zstack/header/network/l3/UsedIpInventory.java +++ b/header/src/main/java/org/zstack/header/network/l3/UsedIpInventory.java @@ -30,6 +30,7 @@ public class UsedIpInventory implements Serializable { private Integer ipVersion; private String ip; private String netmask; + private Integer prefixLen; private String gateway; private String usedFor; @APINoSee @@ -52,6 +53,7 @@ public static UsedIpInventory valueOf(UsedIpVO vo) { inv.setL3NetworkUuid(vo.getL3NetworkUuid()); inv.setGateway(vo.getGateway()); inv.setNetmask(vo.getNetmask()); + inv.setPrefixLen(vo.getPrefixLen()); inv.setUsedFor(vo.getUsedFor()); inv.setVmNicUuid(vo.getVmNicUuid()); inv.setMetaData(vo.getMetaData()); @@ -139,6 +141,14 @@ public void setNetmask(String netmask) { this.netmask = netmask; } + public Integer getPrefixLen() { + return prefixLen; + } + + public void setPrefixLen(Integer prefixLen) { + this.prefixLen = prefixLen; + } + public String getGateway() { return gateway; } diff --git a/header/src/main/java/org/zstack/header/network/l3/UsedIpVO.java b/header/src/main/java/org/zstack/header/network/l3/UsedIpVO.java index c35346301ff..c4f4376fa3b 100755 --- a/header/src/main/java/org/zstack/header/network/l3/UsedIpVO.java +++ b/header/src/main/java/org/zstack/header/network/l3/UsedIpVO.java @@ -24,7 +24,7 @@ public class UsedIpVO { private String uuid; @Column - @ForeignKey(parentEntityClass = IpRangeEO.class, onDeleteAction = ReferenceOption.CASCADE) + @ForeignKey(parentEntityClass = IpRangeEO.class, onDeleteAction = ReferenceOption.SET_NULL) private String ipRangeUuid; @Column @@ -48,6 +48,9 @@ public class UsedIpVO { @Column private String netmask; + @Column + private Integer prefixLen; + @Column @Index private long ipInLong; @@ -147,6 +150,14 @@ public void setNetmask(String netmask) { this.netmask = netmask; } + public Integer getPrefixLen() { + return prefixLen; + } + + public void setPrefixLen(Integer prefixLen) { + this.prefixLen = prefixLen; + } + public String getUsedFor() { return usedFor; } diff --git a/header/src/main/java/org/zstack/header/network/l3/UsedIpVO_.java b/header/src/main/java/org/zstack/header/network/l3/UsedIpVO_.java index 4186a4a6d54..6625e62b12e 100755 --- a/header/src/main/java/org/zstack/header/network/l3/UsedIpVO_.java +++ b/header/src/main/java/org/zstack/header/network/l3/UsedIpVO_.java @@ -16,6 +16,8 @@ public class UsedIpVO_ { public static volatile SingularAttribute ipInLong; public static volatile SingularAttribute vmNicUuid; public static volatile SingularAttribute gateway; + public static volatile SingularAttribute netmask; + public static volatile SingularAttribute prefixLen; public static volatile SingularAttribute createDate; public static volatile SingularAttribute lastOpDate; } diff --git a/header/src/main/java/org/zstack/header/tag/TagPatternInventory.java b/header/src/main/java/org/zstack/header/tag/TagPatternInventory.java index e733faea4eb..c9c34e981e5 100644 --- a/header/src/main/java/org/zstack/header/tag/TagPatternInventory.java +++ b/header/src/main/java/org/zstack/header/tag/TagPatternInventory.java @@ -27,6 +27,8 @@ public class TagPatternInventory { private TagPatternType type; + private String resourceType; + private Timestamp createDate; private Timestamp lastOpDate; @@ -39,6 +41,7 @@ public static TagPatternInventory valueOf(TagPatternVO vo) { inv.value = vo.getValue(); inv.color = vo.getColor(); inv.type = vo.getType(); + inv.resourceType = vo.getResourceType(); inv.createDate = vo.getCreateDate(); inv.lastOpDate = vo.getLastOpDate(); return inv; @@ -111,4 +114,12 @@ public TagPatternType getType() { public void setType(TagPatternType type) { this.type = type; } + + public String getResourceType() { + return resourceType; + } + + public void setResourceType(String resourceType) { + this.resourceType = resourceType; + } } diff --git a/header/src/main/java/org/zstack/header/tag/TagPatternVO.java b/header/src/main/java/org/zstack/header/tag/TagPatternVO.java index fe35482ffa6..1e7b75647e5 100644 --- a/header/src/main/java/org/zstack/header/tag/TagPatternVO.java +++ b/header/src/main/java/org/zstack/header/tag/TagPatternVO.java @@ -30,6 +30,20 @@ public class TagPatternVO extends ResourceVO implements OwnedByAccount { @Transient private String accountUuid; + /** + * Limits this tag pattern to a specific resource type (e.g. "ModelVO"). + *

+ * NULL means the tag pattern is universal — available for all resource types. + * This ensures backward compatibility: tag patterns created before this field + * was introduced (upgraded from older versions) have resourceType=NULL and + * remain visible everywhere. + *

+ * When filtering tag patterns for a specific resource page, use: + * {@code WHERE resourceType IS NULL OR resourceType = :targetResourceType} + */ + @Column + private String resourceType; + @Column private Timestamp createDate; @@ -106,4 +120,12 @@ public TagPatternType getType() { public void setType(TagPatternType type) { this.type = type; } + + public String getResourceType() { + return resourceType; + } + + public void setResourceType(String resourceType) { + this.resourceType = resourceType; + } } diff --git a/header/src/main/java/org/zstack/header/tag/TagPatternVO_.java b/header/src/main/java/org/zstack/header/tag/TagPatternVO_.java index 7cdc57eb5b4..9e1d541808a 100644 --- a/header/src/main/java/org/zstack/header/tag/TagPatternVO_.java +++ b/header/src/main/java/org/zstack/header/tag/TagPatternVO_.java @@ -13,6 +13,7 @@ public class TagPatternVO_ extends ResourceVO_ { public static volatile SingularAttribute description; public static volatile SingularAttribute color; public static volatile SingularAttribute type; + public static volatile SingularAttribute resourceType; public static volatile SingularAttribute createDate; public static volatile SingularAttribute lastOpDate; } diff --git a/header/src/main/java/org/zstack/header/vm/APIChangeVmNicNetworkMsg.java b/header/src/main/java/org/zstack/header/vm/APIChangeVmNicNetworkMsg.java index c00ab47e904..6762f85f793 100644 --- a/header/src/main/java/org/zstack/header/vm/APIChangeVmNicNetworkMsg.java +++ b/header/src/main/java/org/zstack/header/vm/APIChangeVmNicNetworkMsg.java @@ -2,12 +2,16 @@ import org.springframework.http.HttpMethod; import org.zstack.header.identity.Action; +import org.zstack.header.message.APIEvent; import org.zstack.header.message.APIMessage; import org.zstack.header.message.APIParam; import org.zstack.header.network.l3.L3NetworkVO; +import org.zstack.header.other.APIAuditor; +import org.zstack.header.other.APIMultiAuditor; import org.zstack.header.rest.APINoSee; import org.zstack.header.rest.RestRequest; +import java.util.ArrayList; import java.util.List; import java.util.Map; @@ -18,7 +22,7 @@ method = HttpMethod.POST, responseClass = APIChangeVmNicNetworkEvent.class ) -public class APIChangeVmNicNetworkMsg extends APIMessage implements VmInstanceMessage{ +public class APIChangeVmNicNetworkMsg extends APIMessage implements VmInstanceMessage, APIMultiAuditor { @APIParam(resourceType = VmNicVO.class, checkAccount = true, operationTarget = true) private String vmNicUuid; @@ -33,6 +37,10 @@ public class APIChangeVmNicNetworkMsg extends APIMessage implements VmInstanceMe private String staticIp; + + @APIParam(required = false) + private List dnsAddresses; + public String getVmNicUuid() { return vmNicUuid; } @@ -57,6 +65,7 @@ public void setRequiredIpMap(Map> requiredIpMap) { this.requiredIpMap = requiredIpMap; } + public static APIChangeVmNicNetworkMsg __example__() { APIChangeVmNicNetworkMsg msg = new APIChangeVmNicNetworkMsg(); msg.vmNicUuid = uuid(); @@ -80,4 +89,20 @@ public String getStaticIp() { public void setStaticIp(String staticIp) { this.staticIp = staticIp; } + + public List getDnsAddresses() { + return dnsAddresses; + } + + public void setDnsAddresses(List dnsAddresses) { + this.dnsAddresses = dnsAddresses; + } + + @Override + public List multiAudit(APIMessage msg, APIEvent rsp) { + APIChangeVmNicNetworkMsg amsg = (APIChangeVmNicNetworkMsg) msg; + List res = new ArrayList<>(); + res.add(new APIAuditor.Result(amsg.getVmInstanceUuid(), VmInstanceVO.class)); + return res; + } } diff --git a/header/src/main/java/org/zstack/header/vm/APIChangeVmNicNetworkMsgDoc_zh_cn.groovy b/header/src/main/java/org/zstack/header/vm/APIChangeVmNicNetworkMsgDoc_zh_cn.groovy index 893596734c7..c02221bd20e 100644 --- a/header/src/main/java/org/zstack/header/vm/APIChangeVmNicNetworkMsgDoc_zh_cn.groovy +++ b/header/src/main/java/org/zstack/header/vm/APIChangeVmNicNetworkMsgDoc_zh_cn.groovy @@ -66,6 +66,15 @@ doc { optional true since "0.6" } + column { + name "dnsAddresses" + enclosedIn "params" + desc "DNS服务器地址列表" + location "body" + type "List" + optional true + since "5.5.6" + } } } diff --git a/header/src/main/java/org/zstack/header/vm/APISetVmStaticIpMsg.java b/header/src/main/java/org/zstack/header/vm/APISetVmStaticIpMsg.java index a4e0d71b209..80a022b5cfb 100755 --- a/header/src/main/java/org/zstack/header/vm/APISetVmStaticIpMsg.java +++ b/header/src/main/java/org/zstack/header/vm/APISetVmStaticIpMsg.java @@ -7,6 +7,8 @@ import org.zstack.header.network.l3.L3NetworkVO; import org.zstack.header.rest.RestRequest; +import java.util.List; + /** * Created by frank on 2/26/2016. */ @@ -34,6 +36,8 @@ public class APISetVmStaticIpMsg extends APIMessage implements VmInstanceMessage private String ipv6Gateway; @APIParam(required = false) private String ipv6Prefix; + @APIParam(required = false) + private List dnsAddresses; public String getIp() { return ip; @@ -100,6 +104,14 @@ public void setIpv6Prefix(String ipv6Prefix) { this.ipv6Prefix = ipv6Prefix; } + public List getDnsAddresses() { + return dnsAddresses; + } + + public void setDnsAddresses(List dnsAddresses) { + this.dnsAddresses = dnsAddresses; + } + public static APISetVmStaticIpMsg __example__() { APISetVmStaticIpMsg msg = new APISetVmStaticIpMsg(); msg.vmInstanceUuid = uuid(); diff --git a/header/src/main/java/org/zstack/header/vm/APISetVmStaticIpMsgDoc_zh_cn.groovy b/header/src/main/java/org/zstack/header/vm/APISetVmStaticIpMsgDoc_zh_cn.groovy index 9415e501788..20513324725 100644 --- a/header/src/main/java/org/zstack/header/vm/APISetVmStaticIpMsgDoc_zh_cn.groovy +++ b/header/src/main/java/org/zstack/header/vm/APISetVmStaticIpMsgDoc_zh_cn.groovy @@ -114,6 +114,15 @@ doc { optional true since "0.6" } + column { + name "dnsAddresses" + enclosedIn "setVmStaticIp" + desc "DNS服务器地址列表" + location "body" + type "List" + optional true + since "5.5.6" + } } } diff --git a/header/src/main/java/org/zstack/header/vm/ChangeVmNicNetworkMsg.java b/header/src/main/java/org/zstack/header/vm/ChangeVmNicNetworkMsg.java index 1e7f7b1772f..54816bdfcde 100644 --- a/header/src/main/java/org/zstack/header/vm/ChangeVmNicNetworkMsg.java +++ b/header/src/main/java/org/zstack/header/vm/ChangeVmNicNetworkMsg.java @@ -14,6 +14,7 @@ public class ChangeVmNicNetworkMsg extends NeedReplyMessage implements VmInstanc private String vmInstanceUuid; private Map> requiredIpMap; private String staticIp; + private List dnsAddresses; public String getVmNicUuid() { return vmNicUuid; @@ -55,4 +56,13 @@ public String getStaticIp() { public void setStaticIp(String staticIp) { this.staticIp = staticIp; } + + + public List getDnsAddresses() { + return dnsAddresses; + } + + public void setDnsAddresses(List dnsAddresses) { + this.dnsAddresses = dnsAddresses; + } } diff --git a/header/src/main/java/org/zstack/header/vm/SetVmStaticIpMsg.java b/header/src/main/java/org/zstack/header/vm/SetVmStaticIpMsg.java index dd27c3c0c48..a1a117da8a9 100644 --- a/header/src/main/java/org/zstack/header/vm/SetVmStaticIpMsg.java +++ b/header/src/main/java/org/zstack/header/vm/SetVmStaticIpMsg.java @@ -2,6 +2,8 @@ import org.zstack.header.message.NeedReplyMessage; +import java.util.List; + /** * Created by LiangHanYu on 2022/6/22 17:12 */ @@ -14,6 +16,7 @@ public class SetVmStaticIpMsg extends NeedReplyMessage implements VmInstanceMess private String gateway; private String ipv6Gateway; private String ipv6Prefix; + private List dnsAddresses; @Override public String getVmInstanceUuid() { @@ -79,4 +82,12 @@ public String getIpv6Prefix() { public void setIpv6Prefix(String ipv6Prefix) { this.ipv6Prefix = ipv6Prefix; } + + public List getDnsAddresses() { + return dnsAddresses; + } + + public void setDnsAddresses(List dnsAddresses) { + this.dnsAddresses = dnsAddresses; + } } diff --git a/header/src/main/java/org/zstack/header/vm/VmInstanceConstant.java b/header/src/main/java/org/zstack/header/vm/VmInstanceConstant.java index 896e60e414e..fec2e4f1b51 100755 --- a/header/src/main/java/org/zstack/header/vm/VmInstanceConstant.java +++ b/header/src/main/java/org/zstack/header/vm/VmInstanceConstant.java @@ -13,6 +13,7 @@ public interface VmInstanceConstant { // System limit int MAXIMUM_CDROM_NUMBER = 3; + int MAXIMUM_NIC_DNS_NUMBER = 3; String KVM_HYPERVISOR_TYPE = "KVM"; diff --git a/header/src/main/java/org/zstack/header/volume/VolumeProtocol.java b/header/src/main/java/org/zstack/header/volume/VolumeProtocol.java index 8cfebdf6080..aadc4cf15d3 100644 --- a/header/src/main/java/org/zstack/header/volume/VolumeProtocol.java +++ b/header/src/main/java/org/zstack/header/volume/VolumeProtocol.java @@ -5,5 +5,6 @@ public enum VolumeProtocol { iSCSI, Vhost, CBD, - NBD + NBD, + RBD } diff --git a/header/src/main/java/org/zstack/header/volume/VolumeProtocolCapability.java b/header/src/main/java/org/zstack/header/volume/VolumeProtocolCapability.java index c0ce747a9bb..344badbe8a2 100644 --- a/header/src/main/java/org/zstack/header/volume/VolumeProtocolCapability.java +++ b/header/src/main/java/org/zstack/header/volume/VolumeProtocolCapability.java @@ -65,4 +65,12 @@ public boolean isSupportReadonly() { public void setSupportReadonly(boolean supportReadonly) { this.supportReadonly = supportReadonly; } + + public String getProtocol() { + return protocol; + } + + public String getHypervisor() { + return hypervisor; + } } diff --git a/header/src/main/java/org/zstack/header/zql/BeforeCallZWatchExtensionPoint.java b/header/src/main/java/org/zstack/header/zql/BeforeCallZWatchExtensionPoint.java new file mode 100644 index 00000000000..d0ec90621d6 --- /dev/null +++ b/header/src/main/java/org/zstack/header/zql/BeforeCallZWatchExtensionPoint.java @@ -0,0 +1,23 @@ +package org.zstack.header.zql; + +import java.util.List; + +/** + * BeforeCallZWatchExtensionPoint is an extension point that allows plugins + * to perform custom operations before calling zwatch. + */ +public interface BeforeCallZWatchExtensionPoint { + /** + * Check if this extension supports the given VO class + * @param voClass the VO class to check + * @return true if this extension supports the VO class, false otherwise + */ + boolean supports(Class voClass); + + /** + * Perform custom operations before calling ZWatch, for example: health-check + * @param voClass the VO class type + * @param uuids the list of resource UUIDs to process + */ + void beforeCallZWatch(Class voClass, List uuids); +} diff --git a/header/src/main/java/org/zstack/header/zwatch/ResourceMetricBindingExtensionPoint.java b/header/src/main/java/org/zstack/header/zwatch/ResourceMetricBindingExtensionPoint.java new file mode 100644 index 00000000000..1ec04b18fb4 --- /dev/null +++ b/header/src/main/java/org/zstack/header/zwatch/ResourceMetricBindingExtensionPoint.java @@ -0,0 +1,88 @@ +package org.zstack.header.zwatch; + +import java.util.List; + +public interface ResourceMetricBindingExtensionPoint { + class ResourceMetricBinding { + private Class resourceType; + private String logicalMetricName; + private String sourceNamespace; + private String sourceMetricName; + private String resourceField; + private String sourceLabel; + private boolean requireUniqueSourceKey; + + private static T requireValue(String fieldName, T value) { + if (value == null) { + throw new IllegalStateException(String.format("ResourceMetricBinding.%s must not be null", fieldName)); + } + return value; + } + + private static String requireText(String fieldName, String value) { + requireValue(fieldName, value); + if (value.trim().isEmpty()) { + throw new IllegalStateException(String.format("ResourceMetricBinding.%s must not be empty", fieldName)); + } + return value; + } + + public Class getResourceType() { + return requireValue("resourceType", resourceType); + } + + public void setResourceType(Class resourceType) { + this.resourceType = resourceType; + } + + public String getLogicalMetricName() { + return requireText("logicalMetricName", logicalMetricName); + } + + public void setLogicalMetricName(String logicalMetricName) { + this.logicalMetricName = logicalMetricName; + } + + public String getSourceNamespace() { + return requireText("sourceNamespace", sourceNamespace); + } + + public void setSourceNamespace(String sourceNamespace) { + this.sourceNamespace = sourceNamespace; + } + + public String getSourceMetricName() { + return requireText("sourceMetricName", sourceMetricName); + } + + public void setSourceMetricName(String sourceMetricName) { + this.sourceMetricName = sourceMetricName; + } + + public String getResourceField() { + return requireText("resourceField", resourceField); + } + + public void setResourceField(String resourceField) { + this.resourceField = resourceField; + } + + public String getSourceLabel() { + return requireText("sourceLabel", sourceLabel); + } + + public void setSourceLabel(String sourceLabel) { + this.sourceLabel = sourceLabel; + } + + public boolean isRequireUniqueSourceKey() { + return requireUniqueSourceKey; + } + + public void setRequireUniqueSourceKey(boolean requireUniqueSourceKey) { + this.requireUniqueSourceKey = requireUniqueSourceKey; + } + } + + List getResourceMetricBindings(); +} diff --git a/identity/src/main/java/org/zstack/identity/QuotaUtil.java b/identity/src/main/java/org/zstack/identity/QuotaUtil.java index d7b29015624..34fce1b5f80 100644 --- a/identity/src/main/java/org/zstack/identity/QuotaUtil.java +++ b/identity/src/main/java/org/zstack/identity/QuotaUtil.java @@ -72,7 +72,7 @@ public String getResourceOwnerAccountUuid(String resourceUuid) { } @Transactional(readOnly = true) - public void CheckQuota(QuotaCompareInfo quotaCompareInfo) { + public ErrorCode checkQuotaAndReturn(QuotaCompareInfo quotaCompareInfo) { logger.trace(String.format("dump quota QuotaCompareInfo: \n %s", JSONObjectUtil.toJsonString(quotaCompareInfo))); String accountName = Q.New(AccountVO.class) @@ -80,14 +80,23 @@ public void CheckQuota(QuotaCompareInfo quotaCompareInfo) { .eq(AccountVO_.uuid, quotaCompareInfo.resourceTargetOwnerAccountUuid) .findValue(); if (quotaCompareInfo.currentUsed + quotaCompareInfo.request > quotaCompareInfo.quotaValue) { - throw new ApiMessageInterceptionException(err(ORG_ZSTACK_IDENTITY_10002, IdentityErrors.QUOTA_EXCEEDING, + return err(ORG_ZSTACK_IDENTITY_10002, IdentityErrors.QUOTA_EXCEEDING, "quota exceeding." + "The resource owner(or target resource owner) account[uuid: %s name: %s] exceeds a quota[name: %s, value: %s], " + "Current used:%s, Request:%s. Please contact the administrator.", quotaCompareInfo.resourceTargetOwnerAccountUuid, StringUtils.trimToEmpty(accountName), quotaCompareInfo.quotaName, quotaCompareInfo.quotaValue, quotaCompareInfo.currentUsed, quotaCompareInfo.request - )); + ); + } + + return null; + } + + public void CheckQuota(QuotaCompareInfo quotaCompareInfo) { + ErrorCode error = checkQuotaAndReturn(quotaCompareInfo); + if (error != null) { + throw new ApiMessageInterceptionException(error); } } diff --git a/network/src/main/java/org/zstack/network/l2/L2NetworkApiInterceptor.java b/network/src/main/java/org/zstack/network/l2/L2NetworkApiInterceptor.java index 6f0b796e5b8..40786eb7b2e 100755 --- a/network/src/main/java/org/zstack/network/l2/L2NetworkApiInterceptor.java +++ b/network/src/main/java/org/zstack/network/l2/L2NetworkApiInterceptor.java @@ -123,6 +123,12 @@ private void validate(APICreateL2NetworkMsg msg) { } catch (Exception e) { throw new ApiMessageInterceptionException(argerr(ORG_ZSTACK_NETWORK_L2_10012, "unsupported vSwitch type[%s]", msg.getvSwitchType())); } + + if (L2NetworkConstant.VSWITCH_TYPE_LINUX_BRIDGE.equals(msg.getvSwitchType()) + && (msg.getPhysicalInterface() == null || msg.getPhysicalInterface().trim().isEmpty())) { + throw new ApiMessageInterceptionException(argerr(ORG_ZSTACK_NETWORK_L2_10021, + "physicalInterface is required when vSwitchType is [%s]", msg.getvSwitchType())); + } } private void validate(APIChangeL2NetworkVlanIdMsg msg) { diff --git a/network/src/main/java/org/zstack/network/l3/IpRangeHelper.java b/network/src/main/java/org/zstack/network/l3/IpRangeHelper.java index fffbff32665..64cd7854716 100644 --- a/network/src/main/java/org/zstack/network/l3/IpRangeHelper.java +++ b/network/src/main/java/org/zstack/network/l3/IpRangeHelper.java @@ -264,6 +264,95 @@ public static boolean isIpAddressAllocationEnableOnL3(String l3Uuid) { return l3NetworkVO.enableIpAddressAllocation(); } + /** + * Check if an IP address is within any L3 network's CIDR (from NormalIpRange). + */ + public static boolean isIpInL3NetworkCidr(String ip, String l3Uuid) { + if (ip == null || l3Uuid == null) { + return false; + } + + if (IPv6NetworkUtils.isIpv6Address(ip)) { + List ranges = Q.New(NormalIpRangeVO.class) + .eq(NormalIpRangeVO_.l3NetworkUuid, l3Uuid) + .eq(NormalIpRangeVO_.ipVersion, IPv6Constants.IPv6).list(); + for (NormalIpRangeVO ipr : ranges) { + String cidr = ipr.getNetworkCidr(); + if (cidr != null && IPv6NetworkUtils.isIpv6InCidrRange(ip, cidr)) { + return true; + } + } + } else if (NetworkUtils.isIpv4Address(ip)) { + List ranges = Q.New(NormalIpRangeVO.class) + .eq(NormalIpRangeVO_.l3NetworkUuid, l3Uuid) + .eq(NormalIpRangeVO_.ipVersion, IPv6Constants.IPv4).list(); + for (NormalIpRangeVO ipr : ranges) { + String cidr = ipr.getNetworkCidr(); + if (cidr != null && NetworkUtils.isIpv4InCidr(ip, cidr)) { + return true; + } + } + } + + return false; + } + + /** + * Check if an IP address is outside all L3 network CIDRs. + */ + public static boolean isIpOutsideL3NetworkCidr(String ip, String l3Uuid) { + return !isIpInL3NetworkCidr(ip, l3Uuid); + } + + /** + * Find a NormalIpRangeVO whose CIDR contains the given IP. + * First tries exact range match (startIp-endIp), then falls back to CIDR match. + */ + public static NormalIpRangeVO findIpRangeByCidr(String ip, List ranges) { + if (ip == null || ranges == null || ranges.isEmpty()) { + return null; + } + + boolean isIpv4 = NetworkUtils.isIpv4Address(ip); + boolean isIpv6 = IPv6NetworkUtils.isIpv6Address(ip); + int targetVersion = isIpv4 ? IPv6Constants.IPv4 : (isIpv6 ? IPv6Constants.IPv6 : -1); + if (targetVersion == -1) { + return null; + } + + // First try exact range match + for (NormalIpRangeVO ipr : ranges) { + if (ipr.getIpVersion() != targetVersion) { + continue; + } + if (isIpv4 && NetworkUtils.isInRange(ip, ipr.getStartIp(), ipr.getEndIp())) { + return ipr; + } + if (isIpv6 && IPv6NetworkUtils.isIpv6InRange(ip, ipr.getStartIp(), ipr.getEndIp())) { + return ipr; + } + } + + // Fallback to CIDR match + for (NormalIpRangeVO ipr : ranges) { + if (ipr.getIpVersion() != targetVersion) { + continue; + } + String cidr = ipr.getNetworkCidr(); + if (cidr == null) { + continue; + } + if (isIpv4 && NetworkUtils.isIpv4InCidr(ip, cidr)) { + return ipr; + } + if (isIpv6 && IPv6NetworkUtils.isIpv6InCidrRange(ip, cidr)) { + return ipr; + } + } + + return null; + } + public static IpRangeVO fromIpRangeInventory(IpRangeInventory ipr, String accountUuid) { NormalIpRangeVO vo = new NormalIpRangeVO(); vo.setUuid(ipr.getUuid() == null ? Platform.getUuid() : ipr.getUuid()); diff --git a/network/src/main/java/org/zstack/network/l3/L3BasicNetwork.java b/network/src/main/java/org/zstack/network/l3/L3BasicNetwork.java index b1b0b92d497..d7c6c6798d9 100755 --- a/network/src/main/java/org/zstack/network/l3/L3BasicNetwork.java +++ b/network/src/main/java/org/zstack/network/l3/L3BasicNetwork.java @@ -319,6 +319,7 @@ public void fail(ErrorCode errorCode) { @Override public void run(FlowTrigger trigger, Map data) { + SQL.New(UsedIpVO.class).eq(UsedIpVO_.ipRangeUuid, iprvo.getUuid()).delete(); dbf.remove(iprvo); IpRangeHelper.updateL3NetworkIpversion(iprvo); @@ -555,6 +556,8 @@ public void run(SyncTaskChain chain) { return; } + ip = overrideUsedIpIfNeeded(msg, ip); + logger.debug(String.format("Ip allocator strategy[%s] successfully allocates an ip[%s]", strategyType, ip.getIp())); reply.setIpInventory(ip); bus.reply(msg, reply); @@ -568,6 +571,53 @@ public String getName() { }); } + private UsedIpInventory overrideUsedIpIfNeeded(AllocateIpMsg msg, UsedIpInventory ip) { + String overrideNetmask = null; + String overrideGateway = null; + Integer prefixLength = null; + + if (ip.getIpVersion() != null && ip.getIpVersion() == IPv6Constants.IPv4) { + if (msg.getNetmask() != null) { + overrideNetmask = msg.getNetmask(); + } + if (msg.getGateway() != null) { + overrideGateway = msg.getGateway(); + } + } else if (ip.getIpVersion() != null && ip.getIpVersion() == IPv6Constants.IPv6) { + if (msg.getIpv6Prefix() != null) { + try { + prefixLength = Integer.parseInt(msg.getIpv6Prefix()); + overrideNetmask = IPv6NetworkUtils.getFormalNetmaskOfNetworkCidr(ip.getIp() + "/" + msg.getIpv6Prefix()); + } catch (NumberFormatException e) { + logger.warn(String.format("failed to parse prefix length[%s], ignore it and use the default prefix length of the ip range", + msg.getIpv6Prefix())); + } + } + if (msg.getIpv6Gateway() != null) { + overrideGateway = msg.getIpv6Gateway().isEmpty() ? "" : IPv6NetworkUtils.getIpv6AddressCanonicalString(msg.getIpv6Gateway()); + } + } + + if (overrideNetmask != null || overrideGateway != null) { + UsedIpVO vo = dbf.findByUuid(ip.getUuid(), UsedIpVO.class); + if (vo != null) { + if (overrideNetmask != null) { + vo.setNetmask(overrideNetmask); + } + if (overrideGateway != null) { + vo.setGateway(overrideGateway); + } + if (prefixLength != null) { + vo.setPrefixLen(prefixLength); + } + vo = dbf.updateAndRefresh(vo); + ip = UsedIpInventory.valueOf(vo); + } + } + + return ip; + } + private void handleApiMessage(APIMessage msg) { if (msg instanceof APIDeleteL3NetworkMsg) { handle((APIDeleteL3NetworkMsg) msg); @@ -767,10 +817,7 @@ private void handle(APIDeleteIpAddressMsg msg) { @Override public CheckIpAvailabilityReply checkIpAvailability(CheckIpAvailabilityMsg msg) { CheckIpAvailabilityReply reply = new CheckIpAvailabilityReply(); - int ipversion = IPv6Constants.IPv4; - if (IPv6NetworkUtils.isIpv6Address(msg.getIp())) { - ipversion = IPv6Constants.IPv6; - } + final int ipversion = IPv6NetworkUtils.isIpv6Address(msg.getIp()) ? IPv6Constants.IPv6 : IPv6Constants.IPv4; SimpleQuery rq = dbf.createQuery(IpRangeVO.class); rq.select(IpRangeVO_.startIp, IpRangeVO_.endIp, IpRangeVO_.gateway); rq.add(IpRangeVO_.l3NetworkUuid, Op.EQ, self.getUuid()); diff --git a/network/src/main/java/org/zstack/network/l3/L3NetworkApiInterceptor.java b/network/src/main/java/org/zstack/network/l3/L3NetworkApiInterceptor.java index d4dfa4dc8d6..ac88e09dbf8 100755 --- a/network/src/main/java/org/zstack/network/l3/L3NetworkApiInterceptor.java +++ b/network/src/main/java/org/zstack/network/l3/L3NetworkApiInterceptor.java @@ -726,6 +726,48 @@ private void validate(IpRangeInventory ipr) { throw new ApiMessageInterceptionException(argerr(ORG_ZSTACK_NETWORK_L3_10064, "new add ip range gateway %s is different from old gateway %s", ipr.getGateway(), r.getGateway())); } } + + // When adding the first IpRange, check if network address or gateway is already used + if (l3IpRanges.isEmpty()) { + String networkAddress = info.getNetworkAddress(); + String broadcastAddress = info.getBroadcastAddress(); + + // Check if gateway address is already used by VmNic with ipRangeUuid=null + boolean gatewayUsed = Q.New(UsedIpVO.class) + .eq(UsedIpVO_.l3NetworkUuid, ipr.getL3NetworkUuid()) + .eq(UsedIpVO_.ip, ipr.getGateway()) + .isNull(UsedIpVO_.ipRangeUuid) + .isExists(); + if (gatewayUsed) { + throw new ApiMessageInterceptionException(argerr( + ORG_ZSTACK_NETWORK_L3_10079, "gateway address[%s] is already used by a VM NIC, cannot add IP range with this gateway", + ipr.getGateway())); + } + + // Check if network address is already used + boolean networkAddressUsed = Q.New(UsedIpVO.class) + .eq(UsedIpVO_.l3NetworkUuid, ipr.getL3NetworkUuid()) + .eq(UsedIpVO_.ip, networkAddress) + .isNull(UsedIpVO_.ipRangeUuid) + .isExists(); + if (networkAddressUsed) { + throw new ApiMessageInterceptionException(argerr( + ORG_ZSTACK_NETWORK_L3_10080, "network address[%s] is already used by a VM NIC, cannot add IP range containing this address", + networkAddress)); + } + + // Check if broadcast address is already used + boolean broadcastAddressUsed = Q.New(UsedIpVO.class) + .eq(UsedIpVO_.l3NetworkUuid, ipr.getL3NetworkUuid()) + .eq(UsedIpVO_.ip, broadcastAddress) + .isNull(UsedIpVO_.ipRangeUuid) + .isExists(); + if (broadcastAddressUsed) { + throw new ApiMessageInterceptionException(argerr( + ORG_ZSTACK_NETWORK_L3_10081, "broadcast address[%s] is already used by a VM NIC, cannot add IP range containing this address", + broadcastAddress)); + } + } } else if (ipr.getIpRangeType() == IpRangeType.AddressPool) { validateAddressPool(ipr); } diff --git a/network/src/main/java/org/zstack/network/l3/L3NetworkManagerImpl.java b/network/src/main/java/org/zstack/network/l3/L3NetworkManagerImpl.java index 384a5d2c1df..81fb7e94b3f 100755 --- a/network/src/main/java/org/zstack/network/l3/L3NetworkManagerImpl.java +++ b/network/src/main/java/org/zstack/network/l3/L3NetworkManagerImpl.java @@ -383,7 +383,7 @@ public IpCapacity call() { ts = IpRangeHelper.stripNetworkAndBroadcastAddress(ts); calcElementTotalIp(ts, ret); - sql = "select count(distinct uip.ip), uip.l3NetworkUuid, uip.ipVersion from UsedIpVO uip where uip.l3NetworkUuid in (:uuids) and (uip.metaData not in (:notAccountMetaData) or uip.metaData IS NULL) group by uip.l3NetworkUuid, uip.ipVersion"; + sql = "select count(distinct uip.ip), uip.l3NetworkUuid, uip.ipVersion from UsedIpVO uip where uip.l3NetworkUuid in (:uuids) and uip.ipRangeUuid is not null and (uip.metaData not in (:notAccountMetaData) or uip.metaData IS NULL) group by uip.l3NetworkUuid, uip.ipVersion"; TypedQuery cq = dbf.getEntityManager().createQuery(sql, Tuple.class); cq.setParameter("uuids", msg.getL3NetworkUuids()); cq.setParameter("notAccountMetaData", notAccountMetaDatas); @@ -399,7 +399,7 @@ public IpCapacity call() { ts = IpRangeHelper.stripNetworkAndBroadcastAddress(ts); calcElementTotalIp(ts, ret); - sql = "select count(distinct uip.ip), zone.uuid, uip.ipVersion from UsedIpVO uip, L3NetworkVO l3, ZoneVO zone where uip.l3NetworkUuid = l3.uuid and l3.zoneUuid = zone.uuid and zone.uuid in (:uuids) and (uip.metaData not in (:notAccountMetaData) or uip.metaData IS NULL) group by zone.uuid, uip.ipVersion"; + sql = "select count(distinct uip.ip), zone.uuid, uip.ipVersion from UsedIpVO uip, L3NetworkVO l3, ZoneVO zone where uip.l3NetworkUuid = l3.uuid and l3.zoneUuid = zone.uuid and zone.uuid in (:uuids) and uip.ipRangeUuid is not null and (uip.metaData not in (:notAccountMetaData) or uip.metaData IS NULL) group by zone.uuid, uip.ipVersion"; TypedQuery cq = dbf.getEntityManager().createQuery(sql, Tuple.class); cq.setParameter("uuids", msg.getZoneUuids()); cq.setParameter("notAccountMetaData", notAccountMetaDatas); @@ -723,6 +723,7 @@ private UsedIpInventory reserveIpv6(IpRangeVO ipRange, String ip, boolean allowD vo.setL3NetworkUuid(ipRange.getL3NetworkUuid()); vo.setNetmask(ipRange.getNetmask()); vo.setGateway(ipRange.getGateway()); + vo.setPrefixLen(ipRange.getPrefixLen()); vo.setIpVersion(IPv6Constants.IPv6); vo = dbf.persistAndRefresh(vo); return UsedIpInventory.valueOf(vo); diff --git a/network/src/main/java/org/zstack/network/l3/L3NetworkSystemTags.java b/network/src/main/java/org/zstack/network/l3/L3NetworkSystemTags.java index f6712c54053..c0403be58ed 100644 --- a/network/src/main/java/org/zstack/network/l3/L3NetworkSystemTags.java +++ b/network/src/main/java/org/zstack/network/l3/L3NetworkSystemTags.java @@ -1,6 +1,5 @@ package org.zstack.network.l3; -import org.zstack.header.network.l2.L2NetworkVO; import org.zstack.header.network.l3.L3NetworkVO; import org.zstack.header.tag.TagDefinition; import org.zstack.tag.PatternedSystemTag; diff --git a/network/src/main/java/org/zstack/network/l3/NormalIpRangeFactory.java b/network/src/main/java/org/zstack/network/l3/NormalIpRangeFactory.java index 6b7d70d45eb..c052d0a1d69 100644 --- a/network/src/main/java/org/zstack/network/l3/NormalIpRangeFactory.java +++ b/network/src/main/java/org/zstack/network/l3/NormalIpRangeFactory.java @@ -70,9 +70,11 @@ protected NormalIpRangeVO scripts() { IpRangeHelper.updateL3NetworkIpversion(vo); + // Update UsedIpVO records that have ipRangeUuid=null and IP is within the new range List usedIpVos = Q.New(UsedIpVO.class) .eq(UsedIpVO_.l3NetworkUuid, vo.getL3NetworkUuid()) - .eq(UsedIpVO_.ipVersion, vo.getIpVersion()).list(); + .eq(UsedIpVO_.ipVersion, vo.getIpVersion()) + .isNull(UsedIpVO_.ipRangeUuid).list(); List updateVos = new ArrayList<>(); for (UsedIpVO ipvo : usedIpVos) { if (ipvo.getIpVersion() == IPv6Constants.IPv4) { diff --git a/network/src/main/java/org/zstack/network/service/DhcpExtension.java b/network/src/main/java/org/zstack/network/service/DhcpExtension.java index 625c872dc8a..8e37da8b1e9 100755 --- a/network/src/main/java/org/zstack/network/service/DhcpExtension.java +++ b/network/src/main/java/org/zstack/network/service/DhcpExtension.java @@ -136,11 +136,16 @@ private void populateExtensions() { } public boolean isDualStackNicInSingleL3Network(VmNicInventory nic) { - if (nic.getUsedIps().size() < 2) { + // Filter out IPs outside L3 CIDR range + List validIps = nic.getUsedIps().stream() + .filter(ip -> ip.getIpRangeUuid() != null || IpRangeHelper.isIpInL3NetworkCidr(ip.getIp(), ip.getL3NetworkUuid())) + .collect(Collectors.toList()); + + if (validIps.size() < 2) { return false; } - return nic.getUsedIps().stream().map(UsedIpInventory::getL3NetworkUuid).distinct().count() == 1; + return validIps.stream().map(UsedIpInventory::getL3NetworkUuid).distinct().count() == 1; } private DhcpStruct getDhcpStruct(VmInstanceInventory vm, List hostNames, VmNicVO nic, UsedIpVO ip, boolean isDefaultNic) { @@ -194,7 +199,11 @@ private boolean isEnableRa(String l3Uuid) { private void setDualStackNicOfSingleL3Network(DhcpStruct struct, VmNicVO nic) { struct.setIpVersion(IPv6Constants.DUAL_STACK); - List sortedIps = nic.getUsedIps().stream().sorted(Comparator.comparingLong(UsedIpVO::getIpVersionl)).collect(Collectors.toList()); + // Filter out IPs outside L3 CIDR range + List sortedIps = nic.getUsedIps().stream() + .filter(ip -> ip.getIpRangeUuid() != null || IpRangeHelper.isIpInL3NetworkCidr(ip.getIp(), ip.getL3NetworkUuid())) + .sorted(Comparator.comparingLong(UsedIpVO::getIpVersionl)) + .collect(Collectors.toList()); for (UsedIpVO ip : sortedIps) { if (ip.getIpVersion() == IPv6Constants.IPv4) { struct.setGateway(ip.getGateway()); @@ -275,6 +284,11 @@ public List makeDhcpStruct(VmInstanceInventory vm, List> workoutDhcp(VmInstanceS for (VmNicInventory inv : spec.getDestNics()) { VmNicVO vmNicVO = dbf.findByUuid(inv.getUuid(), VmNicVO.class); for (UsedIpVO ip : vmNicVO.getUsedIps()) { + // Skip IPs outside L3 IP range (not managed by DHCP) + if (ip.getIpRangeUuid() == null) { + continue; + } + L3NetworkInventory l3 = l3Map.get(ip.getL3NetworkUuid()); if (l3 == null) { continue; diff --git a/network/src/main/java/org/zstack/network/service/NetworkServiceManager.java b/network/src/main/java/org/zstack/network/service/NetworkServiceManager.java index cd80c9b184f..7d421393afe 100755 --- a/network/src/main/java/org/zstack/network/service/NetworkServiceManager.java +++ b/network/src/main/java/org/zstack/network/service/NetworkServiceManager.java @@ -18,6 +18,16 @@ public interface NetworkServiceManager { void applyNetworkServiceOnChangeIP(VmInstanceSpec spec, NetworkServiceExtensionPoint.NetworkServiceExtensionPosition position, Completion completion); List getL3NetworkDns(String l3NetworkUuid); + /** + * Get DNS servers for a VM NIC. + * Priority: VM NIC system tag > L3 Network DNS + * + * @param vmUuid VM instance UUID + * @param l3NetworkUuid L3 network UUID + * @return List of DNS server addresses + */ + List getVmNicDns(String vmUuid, String l3NetworkUuid); + void enableNetworkService(L3NetworkVO l3VO, NetworkServiceProviderType providerType, NetworkServiceType nsType, List systemTags, Completion completion); diff --git a/network/src/main/java/org/zstack/network/service/NetworkServiceManagerImpl.java b/network/src/main/java/org/zstack/network/service/NetworkServiceManagerImpl.java index 5e353fa1ff2..24ab48a51de 100755 --- a/network/src/main/java/org/zstack/network/service/NetworkServiceManagerImpl.java +++ b/network/src/main/java/org/zstack/network/service/NetworkServiceManagerImpl.java @@ -25,9 +25,12 @@ import org.zstack.header.network.service.*; import org.zstack.header.network.service.NetworkServiceExtensionPoint.NetworkServiceExtensionPosition; import org.zstack.header.vm.*; +import org.zstack.header.tag.SystemTagVO; +import org.zstack.header.tag.SystemTagVO_; import org.zstack.query.QueryFacade; import org.zstack.utils.Utils; import org.zstack.utils.logging.CLogger; +import org.zstack.utils.network.IPv6NetworkUtils; import java.util.*; @@ -483,6 +486,36 @@ public List getL3NetworkDns(String l3NetworkUuid){ return dns; } + @Override + public List getVmNicDns(String vmUuid, String l3NetworkUuid) { + // First try to get DNS from system tag (VM NIC-level custom DNS) + // Tag format: staticDns::{l3NetworkUuid}::{dns1,dns2,dns3} + String tagLike = String.format("staticDns::%s::%%", l3NetworkUuid); + List tags = Q.New(SystemTagVO.class) + .select(SystemTagVO_.tag) + .eq(SystemTagVO_.resourceUuid, vmUuid) + .eq(SystemTagVO_.resourceType, VmInstanceVO.class.getSimpleName()) + .like(SystemTagVO_.tag, tagLike) + .listValues(); + if (tags != null && !tags.isEmpty()) { + String tag = tags.get(0); + // Parse DNS part: staticDns::{l3Uuid}::{dnsStr} + String prefix = String.format("staticDns::%s::", l3NetworkUuid); + if (tag.startsWith(prefix)) { + String dnsStr = tag.substring(prefix.length()); + if (!dnsStr.isEmpty()) { + List dnsList = new ArrayList<>(); + for (String dns : dnsStr.split(",")) { + dnsList.add(IPv6NetworkUtils.ipv6TagValueToAddress(dns)); + } + return dnsList; + } + } + } + // Fall back to L3 network DNS + return getL3NetworkDns(l3NetworkUuid); + } + @Override public void instantiateResourceOnAttachingNic(VmInstanceSpec spec, L3NetworkInventory l3, Completion completion) { preInstantiateVmResource(spec, completion); diff --git a/plugin/applianceVm/src/main/java/org/zstack/appliancevm/ApplianceVmConstant.java b/plugin/applianceVm/src/main/java/org/zstack/appliancevm/ApplianceVmConstant.java index 1d3f453f312..dcada8edeee 100755 --- a/plugin/applianceVm/src/main/java/org/zstack/appliancevm/ApplianceVmConstant.java +++ b/plugin/applianceVm/src/main/java/org/zstack/appliancevm/ApplianceVmConstant.java @@ -31,6 +31,7 @@ public enum BootstrapParams { sshPort, uuid, managementNodeIp, + managementNodeVip, managementNodeCidr, additionalL3Uuids, } diff --git a/plugin/applianceVm/src/main/java/org/zstack/appliancevm/ApplianceVmFacadeImpl.java b/plugin/applianceVm/src/main/java/org/zstack/appliancevm/ApplianceVmFacadeImpl.java index 0faf04c9f65..6d63ce3522f 100755 --- a/plugin/applianceVm/src/main/java/org/zstack/appliancevm/ApplianceVmFacadeImpl.java +++ b/plugin/applianceVm/src/main/java/org/zstack/appliancevm/ApplianceVmFacadeImpl.java @@ -462,6 +462,7 @@ public Map prepareBootstrapInformation(VmInstanceSpec spec) { ret.put(ApplianceVmConstant.BootstrapParams.publicKey.toString(), publicKey); ret.put(BootstrapParams.uuid.toString(), spec.getVmInventory().getUuid()); ret.put(BootstrapParams.managementNodeIp.toString(), Platform.getManagementServerIp()); + ret.put(BootstrapParams.managementNodeVip.toString(), Platform.getManagementServerVip()); ret.put(BootstrapParams.managementNodeCidr.toString(), Platform.getManagementServerCidr()); /* this is only used by ApplianceVmPrepareBootstrapInfoExtensionPoint extension point, will be deleted after extension point */ ret.put(BootstrapParams.additionalL3Uuids.toString(), additionalNics.stream().map(VmNicInventory::getL3NetworkUuid).collect(Collectors.toList())); diff --git a/plugin/applianceVm/src/main/java/org/zstack/appliancevm/ApplianceVmNicTO.java b/plugin/applianceVm/src/main/java/org/zstack/appliancevm/ApplianceVmNicTO.java index c9905a78667..2c31637659d 100755 --- a/plugin/applianceVm/src/main/java/org/zstack/appliancevm/ApplianceVmNicTO.java +++ b/plugin/applianceVm/src/main/java/org/zstack/appliancevm/ApplianceVmNicTO.java @@ -35,9 +35,20 @@ public ApplianceVmNicTO(VmNicInventory inv) { } else { ip6 = uip.getIp(); gateway6 = uip.getGateway(); - NormalIpRangeVO ipRangeVO = Q.New(NormalIpRangeVO.class).eq(NormalIpRangeVO_.uuid, uip.getIpRangeUuid()).find(); - prefixLength = ipRangeVO.getPrefixLen(); - addressMode = ipRangeVO.getAddressMode(); + // First try to use prefixLen from UsedIpInventory (for IP outside range) + if (uip.getPrefixLen() != null) { + prefixLength = uip.getPrefixLen(); + addressMode = IPv6Constants.SLAAC; + } + if (uip.getIpRangeUuid() != null) { + NormalIpRangeVO ipRangeVO = Q.New(NormalIpRangeVO.class).eq(NormalIpRangeVO_.uuid, uip.getIpRangeUuid()).find(); + if (ipRangeVO != null) { + if (prefixLength == null) { + prefixLength = ipRangeVO.getPrefixLen(); + } + addressMode = ipRangeVO.getAddressMode(); + } + } } } /* for virtual router, gateway ip is in the usedIpVO */ diff --git a/plugin/ceph/src/main/java/org/zstack/storage/ceph/primary/CephImageCacheCleaner.java b/plugin/ceph/src/main/java/org/zstack/storage/ceph/primary/CephImageCacheCleaner.java index 81cec9a040c..032b90d0a6b 100755 --- a/plugin/ceph/src/main/java/org/zstack/storage/ceph/primary/CephImageCacheCleaner.java +++ b/plugin/ceph/src/main/java/org/zstack/storage/ceph/primary/CephImageCacheCleaner.java @@ -34,7 +34,7 @@ protected GlobalConfig cleanupIntervalConfig() { @Transactional @Override protected List createShadowImageCacheVOsForNewDeletedAndOld(String psUuid, ImageCacheCleanParam param) { - List staleImageCacheIds = getStaleImageCacheIds(psUuid, false); + List staleImageCacheIds = getStaleImageCacheIds(psUuid, param.includeReadyImage); if (staleImageCacheIds == null || staleImageCacheIds.isEmpty()) { return null; } diff --git a/plugin/ceph/src/main/java/org/zstack/storage/ceph/primary/CephPrimaryStorageBase.java b/plugin/ceph/src/main/java/org/zstack/storage/ceph/primary/CephPrimaryStorageBase.java index 140e94c13a8..c3b01dc3c8b 100755 --- a/plugin/ceph/src/main/java/org/zstack/storage/ceph/primary/CephPrimaryStorageBase.java +++ b/plugin/ceph/src/main/java/org/zstack/storage/ceph/primary/CephPrimaryStorageBase.java @@ -5448,7 +5448,7 @@ private void deleteSnapshotOnPrimaryStorage(final DeleteSnapshotOnPrimaryStorage httpCall(DELETE_SNAPSHOT_PATH, cmd, DeleteSnapshotRsp.class, new ReturnValueCompletion(msg) { @Override public void success(DeleteSnapshotRsp returnValue) { - osdHelper.releaseAvailableCapWithRatio(msg.getSnapshot().getPrimaryStorageInstallPath(), msg.getSnapshot().getSize()); + osdHelper.releaseAvailableCapacity(msg.getSnapshot().getPrimaryStorageInstallPath(), msg.getSnapshot().getSize()); bus.reply(msg, reply); completion.done(); } diff --git a/plugin/eip/src/main/java/org/zstack/network/service/eip/EipApiInterceptor.java b/plugin/eip/src/main/java/org/zstack/network/service/eip/EipApiInterceptor.java index 85e9a357a06..c1ce073cdd8 100755 --- a/plugin/eip/src/main/java/org/zstack/network/service/eip/EipApiInterceptor.java +++ b/plugin/eip/src/main/java/org/zstack/network/service/eip/EipApiInterceptor.java @@ -23,6 +23,7 @@ import org.zstack.header.vm.VmNicHelper; import org.zstack.header.vm.VmNicVO; import org.zstack.header.vm.VmNicVO_; +import org.zstack.network.l3.IpRangeHelper; import org.zstack.network.service.vip.VipNetworkServicesRefVO; import org.zstack.network.service.vip.VipNetworkServicesRefVO_; import org.zstack.network.service.vip.VipState; @@ -202,6 +203,14 @@ public VipVO call() { } else { msg.setUsedIpUuid(nic.getUsedIpUuid()); } + + // Check if the IP is outside L3 CIDR range + UsedIpVO usedIpVO = dbf.findByUuid(msg.getUsedIpUuid(), UsedIpVO.class); + if (usedIpVO != null && IpRangeHelper.isIpOutsideL3NetworkCidr(usedIpVO.getIp(), usedIpVO.getL3NetworkUuid())) { + throw new ApiMessageInterceptionException(argerr(ORG_ZSTACK_NETWORK_SERVICE_EIP_10024, + "cannot bind EIP to IP address[%s] which is outside L3 network CIDR range", + usedIpVO.getIp())); + } } private void validate(APIDetachEipMsg msg) { @@ -304,6 +313,14 @@ private void validate(APICreateEipMsg msg) { if (msg.getUsedIpUuid() != null) { isVipInVmNicSubnet(msg.getVipUuid(), msg.getUsedIpUuid()); + + // Check if the IP is outside L3 CIDR range + UsedIpVO usedIpVO = dbf.findByUuid(msg.getUsedIpUuid(), UsedIpVO.class); + if (usedIpVO != null && IpRangeHelper.isIpOutsideL3NetworkCidr(usedIpVO.getIp(), usedIpVO.getL3NetworkUuid())) { + throw new ApiMessageInterceptionException(argerr(ORG_ZSTACK_NETWORK_SERVICE_EIP_10024, + "cannot bind EIP to IP address[%s] which is outside L3 network CIDR range", + usedIpVO.getIp())); + } } checkNicRule(msg.getVmNicUuid()); diff --git a/plugin/eip/src/main/java/org/zstack/network/service/eip/EipManagerImpl.java b/plugin/eip/src/main/java/org/zstack/network/service/eip/EipManagerImpl.java index 8e23e2a9175..dae4a3e10bf 100755 --- a/plugin/eip/src/main/java/org/zstack/network/service/eip/EipManagerImpl.java +++ b/plugin/eip/src/main/java/org/zstack/network/service/eip/EipManagerImpl.java @@ -34,6 +34,7 @@ import org.zstack.header.query.ExpandedQueryStruct; import org.zstack.header.vm.*; import org.zstack.identity.AccountManager; +import org.zstack.network.l3.IpRangeHelper; import org.zstack.network.l3.L3NetworkManager; import org.zstack.network.service.NetworkServiceManager; import org.zstack.network.service.vip.*; @@ -416,6 +417,15 @@ private List getAttachableVmNicForEip(VipInventory vip, APIGetEi } else { ret = l3Mgr.filterVmNicByIpVersion(VmNicInventory.valueOf(nics), IPv6Constants.IPv4); } + + // Filter out NICs whose primary IP is outside L3 CIDR + final int targetIpVersion = NetworkUtils.isIpv4Address(vip.getIp()) ? IPv6Constants.IPv4 : IPv6Constants.IPv6; + ret = ret.stream().filter(nic -> { + return nic.getUsedIps().stream() + .filter(ip -> ip.getIpVersion() == targetIpVersion) + .anyMatch(ip -> IpRangeHelper.isIpInL3NetworkCidr(ip.getIp(), ip.getL3NetworkUuid())); + }).collect(Collectors.toList()); + return ret; } diff --git a/plugin/expon/src/main/java/org/zstack/expon/ExponStorageFactory.java b/plugin/expon/src/main/java/org/zstack/expon/ExponStorageFactory.java index 4086058d198..db194c9a415 100644 --- a/plugin/expon/src/main/java/org/zstack/expon/ExponStorageFactory.java +++ b/plugin/expon/src/main/java/org/zstack/expon/ExponStorageFactory.java @@ -8,6 +8,7 @@ import org.zstack.header.volume.VolumeInventory; import org.zstack.header.volume.VolumeProtocol; +import java.util.ArrayList; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; @@ -48,7 +49,7 @@ public String getIdentity() { @Override public List getPreferBackupStorageTypes() { - return preferBackupStorageTypes; + return new ArrayList<>(preferBackupStorageTypes); } public void setPreferBackupStorageTypes(List preferBackupStorageTypes) { diff --git a/plugin/flatNetworkProvider/src/main/java/org/zstack/network/service/flat/FlatDhcpBackend.java b/plugin/flatNetworkProvider/src/main/java/org/zstack/network/service/flat/FlatDhcpBackend.java index 962be8fe4ef..5823bb2b9ee 100755 --- a/plugin/flatNetworkProvider/src/main/java/org/zstack/network/service/flat/FlatDhcpBackend.java +++ b/plugin/flatNetworkProvider/src/main/java/org/zstack/network/service/flat/FlatDhcpBackend.java @@ -18,8 +18,7 @@ import org.zstack.core.db.SQL; import org.zstack.core.defer.Defer; import org.zstack.core.defer.Deferred; -import org.zstack.core.thread.SyncTask; -import org.zstack.core.thread.ThreadFacade; +import org.zstack.core.thread.*; import org.zstack.core.upgrade.GrayVersion; import org.zstack.core.workflow.SimpleFlowChain; import org.zstack.header.AbstractService; @@ -121,6 +120,48 @@ public class FlatDhcpBackend extends AbstractService implements NetworkServiceDh private Map getIpStatisticExts = new HashMap<>(); + private static class DhcpApplyRequest { + final String hostUuid; + final List dhcpInfos; + final boolean rebuild; + + DhcpApplyRequest(String hostUuid, List dhcpInfos, boolean rebuild) { + this.hostUuid = hostUuid; + this.dhcpInfos = dhcpInfos; + this.rebuild = rebuild; + } + } + + private class DhcpApplyQueue extends CoalesceQueue { + @Override + protected String getName() { + return "flat-dhcp-apply"; + } + + @Override + protected void executeBatch(List requests, Completion completion) { + if (requests.isEmpty()) { + completion.success(); + return; + } + + String hostUuid = requests.get(0).hostUuid; + + boolean anyRebuild = false; + List mergedInfos = new ArrayList<>(); + for (DhcpApplyRequest req : requests) { + anyRebuild = anyRebuild || req.rebuild; + mergedInfos.addAll(req.dhcpInfos); + } + + logger.debug(String.format("Coalesced %d DHCP apply requests for host[uuid:%s]", requests.size(), hostUuid)); + + applyDhcpToHosts(mergedInfos, hostUuid, anyRebuild, completion); + } + } + + private final DhcpApplyQueue dhcpApplyCoalesceQueue = new DhcpApplyQueue(); + public static final String APPLY_DHCP_PATH = "/flatnetworkprovider/dhcp/apply"; public static final String BATCH_APPLY_DHCP_PATH = "/flatnetworkprovider/dhcp/batchApply"; public static final String PREPARE_DHCP_PATH = "/flatnetworkprovider/dhcp/prepare"; @@ -1991,7 +2032,7 @@ public DhcpInfo call(DhcpStruct arg) { List dns = new ArrayList<>(); List dns6 = new ArrayList<>(); - for (String dnsIp : nwServiceMgr.getL3NetworkDns(arg.getL3Network().getUuid())) { + for (String dnsIp : nwServiceMgr.getVmNicDns(arg.getVmUuid(), arg.getL3Network().getUuid())) { if (NetworkUtils.isIpv4Address(dnsIp)) { dns.add(dnsIp); } else { @@ -2074,7 +2115,9 @@ public void applyDhcpService(List dhcpStructList, VmInstanceSpec spe return; } - applyDhcpToHosts(toDhcpInfo(dhcpStructList), spec.getDestHost().getUuid(), false, completion); + String hostUuid = spec.getDestHost().getUuid(); + DhcpApplyRequest request = new DhcpApplyRequest(hostUuid, toDhcpInfo(dhcpStructList), false); + dhcpApplyCoalesceQueue.submit(hostUuid, request, completion); } private void releaseDhcpService(List info, final String vmUuid, final String hostUuid, final NoErrorCompletion completion) { diff --git a/plugin/flatNetworkProvider/src/main/java/org/zstack/network/service/flat/FlatEipBackend.java b/plugin/flatNetworkProvider/src/main/java/org/zstack/network/service/flat/FlatEipBackend.java index 5f0b1fb23e7..38a88a84950 100755 --- a/plugin/flatNetworkProvider/src/main/java/org/zstack/network/service/flat/FlatEipBackend.java +++ b/plugin/flatNetworkProvider/src/main/java/org/zstack/network/service/flat/FlatEipBackend.java @@ -439,8 +439,15 @@ public EipTO call(EipVO eip) { to.nicIp = ip.getIp(); to.nicGateway = ip.getGateway(); to.nicNetmask = ip.getNetmask(); - NormalIpRangeVO ipr = Q.New(NormalIpRangeVO.class).eq(NormalIpRangeVO_.uuid, ip.getIpRangeUuid()).find(); - to.nicPrefixLen = ipr.getPrefixLen(); + // First try to use prefixLen from UsedIpVO (for IP outside range) + if (ip.getPrefixLen() != null) { + to.nicPrefixLen = ip.getPrefixLen(); + } else if (ip.getIpRangeUuid() != null) { + NormalIpRangeVO ipr = Q.New(NormalIpRangeVO.class).eq(NormalIpRangeVO_.uuid, ip.getIpRangeUuid()).find(); + if (ipr != null) { + to.nicPrefixLen = ipr.getPrefixLen(); + } + } to.vmBridgeName = bridgeNames.get(ip.getL3NetworkUuid()); } } diff --git a/plugin/iscsi/src/main/java/org/zstack/iscsi/kvm/KvmIscsiNodeServer.java b/plugin/iscsi/src/main/java/org/zstack/iscsi/kvm/KvmIscsiNodeServer.java index 66936e05dc0..0f773d193e7 100644 --- a/plugin/iscsi/src/main/java/org/zstack/iscsi/kvm/KvmIscsiNodeServer.java +++ b/plugin/iscsi/src/main/java/org/zstack/iscsi/kvm/KvmIscsiNodeServer.java @@ -20,10 +20,8 @@ import org.zstack.header.host.HostInventory; import org.zstack.header.host.HostVO; import org.zstack.header.message.MessageReply; -import org.zstack.header.storage.addon.primary.BaseVolumeInfo; -import org.zstack.header.storage.addon.primary.HeartbeatVolumeTO; -import org.zstack.header.storage.addon.primary.HeartbeatVolumeTopology; -import org.zstack.header.storage.addon.primary.PrimaryStorageNodeSvc; +import org.zstack.header.storage.addon.primary.*; +import org.zstack.storage.addon.primary.ExternalHostIdGetter; import org.zstack.header.vm.VmInstanceInventory; import org.zstack.header.vm.VmInstanceMigrateExtensionPoint; import org.zstack.header.vm.VmInstanceSpec; @@ -38,6 +36,8 @@ import org.zstack.kvm.*; import org.zstack.storage.addon.primary.ExternalPrimaryStorageFactory; import org.zstack.utils.DebugUtils; +import org.zstack.utils.Utils; +import org.zstack.utils.logging.CLogger; import java.util.ArrayList; import java.util.List; @@ -49,6 +49,8 @@ public class KvmIscsiNodeServer implements Component, KVMStartVmExtensionPoint, VmInstanceMigrateExtensionPoint, KVMConvertVolumeExtensionPoint, KVMDetachVolumeExtensionPoint, KVMAttachVolumeExtensionPoint, KVMPreAttachIsoExtensionPoint, KvmSetupSelfFencerExtensionPoint { + private static final CLogger logger = Utils.getLogger(KvmIscsiNodeServer.class); + @Autowired private ExternalPrimaryStorageFactory extPsFactory; @@ -235,13 +237,24 @@ public void fail(ErrorCode errorCode) { @Override public void run(FlowTrigger trigger, Map data) { + ExternalPrimaryStorageHostRefVO ref = Q.New(ExternalPrimaryStorageHostRefVO.class) + .eq(ExternalPrimaryStorageHostRefVO_.hostUuid, param.getHostUuid()) + .eq(ExternalPrimaryStorageHostRefVO_.primaryStorageUuid, param.getPrimaryStorage().getUuid()) + .find(); + if (ref == null || ref.getHostId() == 0) { + logger.warn(String.format("not found hostId for hostUuid[%s] and primaryStorageUuid[%s]", + param.getHostUuid(), param.getPrimaryStorage().getUuid())); + ref = new ExternalHostIdGetter(999).getOrAllocateHostIdRef( + param.getHostUuid(), param.getPrimaryStorage().getUuid()); + } + KvmSetupSelfFencerCmd cmd = new KvmSetupSelfFencerCmd(); cmd.interval = param.getInterval(); cmd.maxAttempts = param.getMaxAttempts(); cmd.coveringPaths = heartbeatVol.getCoveringPaths(); cmd.heartbeatUrl = heartbeatVol.getInstallPath(); cmd.storageCheckerTimeout = param.getStorageCheckerTimeout(); - cmd.hostId = heartbeatVol.getHostId(); + cmd.hostId = ref.getHostId(); cmd.heartbeatRequiredSpace = heartbeatVol.getHeartbeatRequiredSpace(); cmd.hostUuid = param.getHostUuid(); cmd.strategy = param.getStrategy(); diff --git a/plugin/kvm/src/main/java/org/zstack/kvm/KVMAgentCommands.java b/plugin/kvm/src/main/java/org/zstack/kvm/KVMAgentCommands.java index a8a1378288b..90e576cad7f 100755 --- a/plugin/kvm/src/main/java/org/zstack/kvm/KVMAgentCommands.java +++ b/plugin/kvm/src/main/java/org/zstack/kvm/KVMAgentCommands.java @@ -2283,6 +2283,8 @@ public static class StartVmCmd extends vdiCmd implements VmAddOnsCmd { private boolean isApplianceVm; @GrayVersion(value = "5.0.0") private String systemSerialNumber; + @GrayVersion(value = "5.5.12") + private String guestOsType; @GrayVersion(value = "5.0.0") private String bootMode; // used when bootMode == 'UEFI' @@ -2312,6 +2314,8 @@ public static class StartVmCmd extends vdiCmd implements VmAddOnsCmd { private boolean consoleLogToFile; @GrayVersion(value = "5.0.0") private boolean acpi; + @GrayVersion(value = "5.5.12") + private boolean pmu = true; @GrayVersion(value = "5.0.0") private boolean x2apic = true; // cpuid hypervisor feature @@ -2474,6 +2478,14 @@ public void setSystemSerialNumber(String systemSerialNumber) { this.systemSerialNumber = systemSerialNumber; } + public String getGuestOsType() { + return guestOsType; + } + + public void setGuestOsType(String guestOsType) { + this.guestOsType = guestOsType; + } + public String getVmCpuModel() { return vmCpuModel; } @@ -2835,6 +2847,14 @@ public void setAcpi(boolean acpi) { this.acpi = acpi; } + public boolean isPmu() { + return pmu; + } + + public void setPmu(boolean pmu) { + this.pmu = pmu; + } + public boolean getX2apic() { return x2apic; } @@ -3805,6 +3825,26 @@ public static class MigrateVmCmd extends AgentCommand implements HasThreadContex private boolean reload; @GrayVersion(value = "5.0.0") private long bandwidth; + @GrayVersion(value = "5.5.12") + private boolean useTls; + @GrayVersion(value = "5.5.12") + private String srcHostManagementIp; + + public String getSrcHostManagementIp() { + return srcHostManagementIp; + } + + public void setSrcHostManagementIp(String srcHostManagementIp) { + this.srcHostManagementIp = srcHostManagementIp; + } + + public boolean isUseTls() { + return useTls; + } + + public void setUseTls(boolean useTls) { + this.useTls = useTls; + } public Integer getDownTime() { return downTime; diff --git a/plugin/kvm/src/main/java/org/zstack/kvm/KVMConstant.java b/plugin/kvm/src/main/java/org/zstack/kvm/KVMConstant.java index 1b2df9f8f2a..314ff983470 100755 --- a/plugin/kvm/src/main/java/org/zstack/kvm/KVMConstant.java +++ b/plugin/kvm/src/main/java/org/zstack/kvm/KVMConstant.java @@ -81,6 +81,7 @@ public interface KVMConstant { String KVM_DELETE_CONSOLE_FIREWALL_PATH = "/vm/console/deletefirewall"; String KVM_UPDATE_HOST_OS_PATH = "/host/updateos"; String KVM_HOST_UPDATE_DEPENDENCY_PATH = "/host/updatedependency"; + String HOST_SHUTDOWN = "/host/shutdown"; String HOST_REBOOT = "/host/reboot"; String HOST_UPDATE_SPICE_CHANNEL_CONFIG_PATH = "/host/updateSpiceChannelConfig"; @@ -96,6 +97,11 @@ public interface KVMConstant { String CLEAN_FIRMWARE_FLASH = "/clean/firmware/flash"; String FSTRIM_VM_PATH = "/vm/fstrim"; + // ZSTAC-83157: virtiofs model mount paths + String KVM_VIRTIOFS_ATTACH_PATH = "/virtiofs/attach"; + String KVM_VIRTIOFS_DETACH_PATH = "/virtiofs/detach"; + String KVM_MODEL_CENTER_MOUNT_PATH = "/modelcenter/mount"; + String ISO_TO = "kvm.isoto"; String ANSIBLE_PLAYBOOK_NAME = "kvm.py"; String ANSIBLE_MODULE_PATH = "ansible/kvm"; diff --git a/plugin/kvm/src/main/java/org/zstack/kvm/KVMGlobalConfig.java b/plugin/kvm/src/main/java/org/zstack/kvm/KVMGlobalConfig.java index 8cdd2f54167..ee765aad40f 100755 --- a/plugin/kvm/src/main/java/org/zstack/kvm/KVMGlobalConfig.java +++ b/plugin/kvm/src/main/java/org/zstack/kvm/KVMGlobalConfig.java @@ -139,6 +139,10 @@ public class KVMGlobalConfig { @BindResourceConfig({HostVO.class, ClusterVO.class}) public static GlobalConfig RECONNECT_HOST_RESTART_LIBVIRTD_SERVICE = new GlobalConfig(CATEGORY, "reconnect.host.restart.libvirtd.service"); + @GlobalConfigValidation(validValues = {"true", "false"}) + @GlobalConfigDef(defaultValue = "true", type = Boolean.class, description = "enable TLS encryption for libvirt remote connections (migration)") + public static GlobalConfig LIBVIRT_TLS_ENABLED = new GlobalConfig(CATEGORY, "libvirt.tls.enabled"); + @GlobalConfigValidation public static GlobalConfig KVMAGENT_PHYSICAL_MEMORY_USAGE_ALARM_THRESHOLD = new GlobalConfig(CATEGORY, "kvmagent.physicalmemory.usage.alarm.threshold"); diff --git a/plugin/kvm/src/main/java/org/zstack/kvm/KVMHost.java b/plugin/kvm/src/main/java/org/zstack/kvm/KVMHost.java index a245757517d..8696757bc92 100755 --- a/plugin/kvm/src/main/java/org/zstack/kvm/KVMHost.java +++ b/plugin/kvm/src/main/java/org/zstack/kvm/KVMHost.java @@ -33,6 +33,7 @@ import org.zstack.core.db.SQLBatch; import org.zstack.core.db.SimpleQuery; import org.zstack.core.db.SimpleQuery.Op; +import org.zstack.core.jsonlabel.JsonLabel; import org.zstack.core.thread.*; import org.zstack.core.timeout.ApiTimeoutManager; import org.zstack.core.timeout.TimeHelper; @@ -66,6 +67,7 @@ import org.zstack.header.message.MessageReply; import org.zstack.header.message.NeedReplyMessage; import org.zstack.header.network.l2.*; +import org.zstack.header.os.OSArchitecture; import org.zstack.header.network.l3.L3NetworkInventory; import org.zstack.header.network.l3.L3NetworkVO; import org.zstack.header.rest.JsonAsyncRESTCallback; @@ -127,6 +129,20 @@ public class KVMHost extends HostBase implements Host { protected static OperationChecker allowedOperations = new OperationChecker(true); protected static OperationChecker skipOperations = new OperationChecker(true); + public static Set parseSanIps(String sanOutput) { + Set sanIps = new HashSet<>(); + if (sanOutput == null || sanOutput.isEmpty()) { + return sanIps; + } + for (String line : sanOutput.split(",|\n")) { + String trimmed = line.trim(); + if (trimmed.startsWith("IP Address:")) { + sanIps.add(trimmed.substring("IP Address:".length()).trim()); + } + } + return sanIps; + } + @Autowired @Qualifier("KVMHostFactory") protected KVMHostFactory factory; @@ -3163,6 +3179,7 @@ public void run(final FlowTrigger trigger, Map data) { cmd.setDestHostIp(dstHostMigrateIp); cmd.setSrcHostIp(srcHostMigrateIp); cmd.setDestHostManagementIp(dstHostMnIp); + cmd.setSrcHostManagementIp(srcHostMnIp); cmd.setMigrateFromDestination(migrateFromDestination); cmd.setStorageMigrationPolicy(storageMigrationPolicy == null ? null : storageMigrationPolicy.toString()); cmd.setVmUuid(vmUuid); @@ -3174,6 +3191,8 @@ public void run(final FlowTrigger trigger, Map data) { cmd.setDownTime(s.downTime); cmd.setBandwidth(s.bandwidth); cmd.setNics(nicTos); + cmd.setUseTls(KVMGlobalConfig.LIBVIRT_TLS_ENABLED.value(Boolean.class) + && rcf.getResourceConfigValue(KVMGlobalConfig.RECONNECT_HOST_RESTART_LIBVIRTD_SERVICE, self.getUuid(), Boolean.class)); if (s.diskMigrationMap != null) { Map diskMigrationMap = new HashMap<>(); @@ -4432,6 +4451,7 @@ protected void startVm(final VmInstanceSpec spec, final NeedReplyMessage msg, fi cmd.setAdditionalQmp(VmGlobalConfig.ADDITIONAL_QMP.value(Boolean.class)); cmd.setApplianceVm(spec.getVmInventory().getType().equals("ApplianceVm")); cmd.setSystemSerialNumber(makeAndSaveVmSystemSerialNumber(spec.getVmInventory().getUuid())); + cmd.setGuestOsType(spec.getVmInventory().getGuestOsType()); if (!NetworkGlobalProperty.CHASSIS_ASSET_TAG.isEmpty()) { cmd.setChassisAssetTag(NetworkGlobalProperty.CHASSIS_ASSET_TAG); } @@ -4570,6 +4590,14 @@ protected void startVm(final VmInstanceSpec spec, final NeedReplyMessage msg, fi cmd.setCreatePaused(true); } cmd.setAcpi(true); + // aarch64: disable PMU by default to avoid kernel panic on new Kunpeng-920 (7270Z/5230Z) + // where PMMIR_EL1 register is not supported by KVM. See ZSTAC-76375 + // GlobalConfig vm.pmu defaults to false; users can re-enable via ResourceConfig. + if (OSArchitecture.AARCH64.normalizedArchName().equals(architecture)) { + Boolean pmuEnabled = rcf.getResourceConfigValue( + VmGlobalConfig.VM_PMU, spec.getVmInventory().getUuid(), Boolean.class); + cmd.setPmu(Boolean.TRUE.equals(pmuEnabled)); + } GuestOsCharacter.Config config = GuestOsHelper.getInstance().getGuestOsCharacter( spec.getVmInventory().getArchitecture(), @@ -5570,10 +5598,10 @@ public boolean skip(Map data) { @Override public void run(FlowTrigger trigger, Map data) { + Ssh ssh = new Ssh().setUsername(getSelf().getUsername()) + .setPassword(getSelf().getPassword()).setPort(getSelf().getPort()) + .setHostname(getSelf().getManagementIp()); try { - Ssh ssh = new Ssh().setUsername(getSelf().getUsername()) - .setPassword(getSelf().getPassword()).setPort(getSelf().getPort()) - .setHostname(getSelf().getManagementIp()); ssh.command(String.format("grep -i ^uuid %s | sed 's/uuid://g'", hostTakeOverFlagPath)); SshResult hostRet = ssh.run(); if (hostRet.isSshFailure() || hostRet.getReturnCode() != 0) { @@ -5622,6 +5650,8 @@ public void run(FlowTrigger trigger, Map data) { logger.warn(e.getMessage(), e); trigger.next(); return; + } finally { + ssh.close(); } } }); @@ -5681,6 +5711,84 @@ public void run(FlowTrigger trigger, Map data) { } }); + flow(new NoRollbackFlow() { + String __name__ = "check-tls-certs-if-needed"; + + @Override + public boolean skip(Map data) { + return CoreGlobalProperty.UNIT_TEST_ON + || !KVMGlobalConfig.LIBVIRT_TLS_ENABLED.value(Boolean.class) + || !rcf.getResourceConfigValue( + KVMGlobalConfig.RECONNECT_HOST_RESTART_LIBVIRTD_SERVICE, + self.getUuid(), Boolean.class); + } + + @Override + public void run(FlowTrigger trigger, Map data) { + String managementIp = getSelf().getManagementIp(); + + // 1. Get all IPs on the host via SSH + SshShell sshShell = new SshShell(); + sshShell.setHostname(managementIp); + sshShell.setUsername(getSelf().getUsername()); + sshShell.setPassword(getSelf().getPassword()); + sshShell.setPort(getSelf().getPort()); + + SshResult ipResult = sshShell.runCommand( + "ip -4 -o addr show scope global | sed -n \"s/.* inet \\([0-9.]\\+\\).*/\\1/p\""); + if (ipResult.isSshFailure() || ipResult.getReturnCode() != 0) { + logger.warn(String.format("Failed to get host IPs via SSH for TLS cert check on host[uuid:%s]: %s", + self.getUuid(), ipResult.getExitErrorMessage())); + trigger.next(); + return; + } + + // 2. Build IP list: managementIp + extra IPs (exclude managementIp and MN VIP) + List allIps = new ArrayList<>(); + allIps.add(managementIp); + String[] hostIps = ipResult.getStdout().trim().split("\n"); + for (String ip : hostIps) { + String trimmed = ip.trim(); + if (!trimmed.isEmpty() && !trimmed.equals(managementIp) + && !trimmed.equals(CoreGlobalProperty.MN_VIP) + && !trimmed.equals("127.0.0.1") + && !allIps.contains(trimmed)) { + allIps.add(trimmed); + } + } + + String certIpList = String.join(",", allIps); + + // 3. Check existing cert SAN via SSH + SshResult sanResult = sshShell.runCommand( + "openssl x509 -in /etc/pki/libvirt/servercert.pem -noout -ext subjectAltName 2>/dev/null"); + + boolean needDeploy = false; + if (sanResult.isSshFailure() || sanResult.getReturnCode() != 0 + || sanResult.getStdout() == null || sanResult.getStdout().trim().isEmpty()) { + // cert doesn't exist or can't be read + logger.info(String.format("TLS cert not found or unreadable on host[uuid:%s], need deploy", self.getUuid())); + needDeploy = true; + } else { + Set sanIps = parseSanIps(sanResult.getStdout()); + for (String ip : allIps) { + if (!sanIps.contains(ip)) { + logger.info(String.format("TLS cert SAN missing IP %s on host[uuid:%s], need deploy", ip, self.getUuid())); + needDeploy = true; + break; + } + } + } + + if (needDeploy) { + data.put("NEED_DEPLOY_TLS_CERT", true); + } + data.put("TLS_CERT_IPS", certIpList); + + trigger.next(); + } + }); + flow(new NoRollbackFlow() { String __name__ = "apply-ansible-playbook"; @@ -5815,6 +5923,29 @@ public void run(final FlowTrigger trigger, Map data) { deployArguments.setSkipPackages(info.getSkipPackages()); deployArguments.setUpdatePackages(String.valueOf(CoreGlobalProperty.UPDATE_PKG_WHEN_CONNECT)); + // Build TLS cert IP list: prefer SSH-detected IPs from check-tls-certs flow + String tlsCertIpsFromData = (String) data.get("TLS_CERT_IPS"); + if (tlsCertIpsFromData != null) { + deployArguments.setTlsCertIps(tlsCertIpsFromData); + } else { + // Fallback: management IP + extra IPs from system tag + String managementIp = getSelf().getManagementIp(); + String extraIps = HostSystemTags.EXTRA_IPS.getTokenByResourceUuid( + self.getUuid(), HostSystemTags.EXTRA_IPS_TOKEN); + if (extraIps != null && !extraIps.isEmpty()) { + deployArguments.setTlsCertIps(managementIp + "," + extraIps); + } else { + deployArguments.setTlsCertIps(managementIp); + } + } + + // Force ansible deploy when TLS cert needs update (detected by check-tls-certs flow) + Boolean needDeployTlsCert = (Boolean) data.get("NEED_DEPLOY_TLS_CERT"); + if (Boolean.TRUE.equals(needDeployTlsCert)) { + runner.setForceRun(true); + deployArguments.setRestartLibvirtd("true"); + } + if (deployArguments.isForceRun()) { runner.setForceRun(true); } @@ -6000,6 +6131,7 @@ public void fail(ErrorCode errorCode) { flow(createCollectHostFactsFlow(info)); + if (info.isNewAdded()) { flow(new NoRollbackFlow() { String __name__ = "check-qemu-libvirt-version"; diff --git a/plugin/kvm/src/main/java/org/zstack/kvm/KVMHostDeployArguments.java b/plugin/kvm/src/main/java/org/zstack/kvm/KVMHostDeployArguments.java index 71fb8a9769e..f2bb79c110b 100644 --- a/plugin/kvm/src/main/java/org/zstack/kvm/KVMHostDeployArguments.java +++ b/plugin/kvm/src/main/java/org/zstack/kvm/KVMHostDeployArguments.java @@ -39,6 +39,8 @@ public class KVMHostDeployArguments extends SyncTimeRequestedDeployArguments { private String restartLibvirtd; @SerializedName("extra_packages") private String extraPackages; + @SerializedName("tls_cert_ips") + private String tlsCertIps; private transient boolean forceRun = false; @@ -135,6 +137,14 @@ public void setExtraPackages(String extraPackages) { this.extraPackages = extraPackages; } + public String getTlsCertIps() { + return tlsCertIps; + } + + public void setTlsCertIps(String tlsCertIps) { + this.tlsCertIps = tlsCertIps; + } + public String getEnableSpiceTls() { return enableSpiceTls; } diff --git a/plugin/kvm/src/main/java/org/zstack/kvm/KVMHostFactory.java b/plugin/kvm/src/main/java/org/zstack/kvm/KVMHostFactory.java index acb129fe6dc..0188c920a83 100755 --- a/plugin/kvm/src/main/java/org/zstack/kvm/KVMHostFactory.java +++ b/plugin/kvm/src/main/java/org/zstack/kvm/KVMHostFactory.java @@ -1,5 +1,6 @@ package org.zstack.kvm; +import org.apache.commons.io.FileUtils; import org.apache.commons.lang.StringUtils; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.web.util.UriComponentsBuilder; @@ -9,6 +10,8 @@ import org.zstack.compute.vm.VmNicManager; import org.zstack.core.CoreGlobalProperty; import org.zstack.core.ansible.AnsibleFacade; +import org.zstack.core.jsonlabel.JsonLabel; +import org.zstack.core.jsonlabel.JsonLabelInventory; import org.zstack.core.cloudbus.CloudBus; import org.zstack.core.cloudbus.CloudBusListCallBack; import org.zstack.core.cloudbus.CloudBusSteppingCallback; @@ -75,6 +78,7 @@ import org.zstack.resourceconfig.ResourceConfigFacade; import org.zstack.utils.CollectionUtils; import org.zstack.utils.IpRangeSet; +import org.zstack.utils.ShellUtils; import org.zstack.utils.SizeUtils; import org.zstack.utils.Utils; import org.zstack.utils.data.SizeUnit; @@ -85,6 +89,7 @@ import org.zstack.utils.logging.CLogger; import javax.persistence.Tuple; +import java.io.File; import java.io.IOException; import java.net.InetSocketAddress; import java.net.SocketAddress; @@ -122,6 +127,10 @@ public class KVMHostFactory extends AbstractService implements HypervisorFactory HypervisorMessageFactory { private static final CLogger logger = Utils.getLogger(KVMHostFactory.class); + private static final String LIBVIRT_TLS_CA_KEY = "libvirtTLSCA"; + private static final String LIBVIRT_TLS_PRIVATE_KEY = "libvirtTLSPrivateKey"; + private static final String CA_DIR = "/var/lib/zstack/pki/CA"; + public static final HypervisorType hypervisorType = new HypervisorType(KVMConstant.KVM_HYPERVISOR_TYPE); public static final VolumeFormat QCOW2_FORMAT = new VolumeFormat(VolumeConstant.VOLUME_FORMAT_QCOW2, hypervisorType); public static final VolumeFormat RAW_FORMAT = new VolumeFormat(VolumeConstant.VOLUME_FORMAT_RAW, hypervisorType); @@ -458,8 +467,56 @@ private void processKvmagentPhysicalMemUsageAbnormal(HostProcessPhysicalMemoryUs bus.send(restartKvmAgentMsg); } + private void initLibvirtTlsCA() { + if (CoreGlobalProperty.UNIT_TEST_ON) { + return; + } + + try { + ShellUtils.run(String.format("mkdir -p %s", CA_DIR)); + ShellUtils.run("chown -R zstack:zstack /var/lib/zstack/pki"); + + File caFile = new File(CA_DIR + "/cacert.pem"); + File keyFile = new File(CA_DIR + "/cakey.pem"); + + // Local CA missing — generate with openssl + // NOTE: ShellUtils.run() prepends sudo only to the first command in &&-chains, + // so each command must be a separate call. + if (!caFile.exists() || !keyFile.exists()) { + ShellUtils.run(String.format( + "openssl genrsa -out %s/cakey.pem 4096", CA_DIR)); + ShellUtils.run(String.format( + "openssl req -new -x509 -days 3650 -key %s/cakey.pem " + + "-out %s/cacert.pem -subj '/O=ZStack/CN=ZStack Libvirt CA'", + CA_DIR, CA_DIR)); + ShellUtils.run(String.format("chown zstack:zstack %s/cakey.pem %s/cacert.pem", + CA_DIR, CA_DIR)); + ShellUtils.run(String.format("chmod 600 %s/cakey.pem", CA_DIR)); + ShellUtils.run(String.format("chmod 644 %s/cacert.pem", CA_DIR)); + } + + String ca = FileUtils.readFileToString(caFile).trim(); + String key = FileUtils.readFileToString(keyFile).trim(); + + // createIfAbsent: DB has no record → write; DB has record → return DB value + JsonLabelInventory caInv = new JsonLabel().createIfAbsent(LIBVIRT_TLS_CA_KEY, ca); + JsonLabelInventory keyInv = new JsonLabel().createIfAbsent(LIBVIRT_TLS_PRIVATE_KEY, key); + + // Use DB as source of truth — overwrite local files (HA: MN2 uses MN1's CA from DB) + FileUtils.writeStringToFile(caFile, caInv.getLabelValue()); + FileUtils.writeStringToFile(keyFile, keyInv.getLabelValue()); + ShellUtils.run(String.format("chmod 600 %s/cakey.pem", CA_DIR)); + ShellUtils.run(String.format("chmod 644 %s/cacert.pem", CA_DIR)); + + logger.info("Libvirt TLS CA initialized and persisted to database"); + } catch (Exception e) { + logger.warn("Failed to initialize libvirt TLS CA", e); + } + } + @Override public boolean start() { + initLibvirtTlsCA(); deployAnsibleModule(); populateExtensions(); configKVMDeviceType(); diff --git a/plugin/kvm/src/main/java/org/zstack/kvm/hypervisor/HypervisorMetadataCollectorImpl.java b/plugin/kvm/src/main/java/org/zstack/kvm/hypervisor/HypervisorMetadataCollectorImpl.java index 468025b01bd..7118fe89bb4 100644 --- a/plugin/kvm/src/main/java/org/zstack/kvm/hypervisor/HypervisorMetadataCollectorImpl.java +++ b/plugin/kvm/src/main/java/org/zstack/kvm/hypervisor/HypervisorMetadataCollectorImpl.java @@ -131,7 +131,8 @@ private boolean collectHypervisorMetadata(HypervisorMetadataDefinition definitio Object platformDistName = properties.get(KEY_PLATFORM_DIST_NAME); Object platformVersion = properties.get(KEY_PLATFORM_VERSION); if (platformDistName != null && platformVersion != null) { - definition.setOsReleaseVersion(String.format("%s %s", platformDistName, platformVersion)); + definition.setOsReleaseVersion(String.format("%s %s", + platformDistName, KvmHypervisorInfoHelper.normalizeOsVersion(platformVersion.toString()))); } else { return false; } diff --git a/plugin/kvm/src/main/java/org/zstack/kvm/hypervisor/KvmHypervisorInfoHelper.java b/plugin/kvm/src/main/java/org/zstack/kvm/hypervisor/KvmHypervisorInfoHelper.java index 4c0b3613024..9ae81bb114c 100644 --- a/plugin/kvm/src/main/java/org/zstack/kvm/hypervisor/KvmHypervisorInfoHelper.java +++ b/plugin/kvm/src/main/java/org/zstack/kvm/hypervisor/KvmHypervisorInfoHelper.java @@ -53,7 +53,7 @@ public static Map collectExpectedHypervisorInfoForHost } HostOperationSystem os = hostOsMap.get(hostUuid); - String osReleaseVersion = String.format("%s %s", os.distribution, os.version); + String osReleaseVersion = String.format("%s %s", os.distribution, normalizeOsVersion(os.version)); Pair key = new Pair<>(architecture, osReleaseVersion); HostOsCategoryVO vo = caches.get(key); @@ -85,6 +85,30 @@ public static Map collectExpectedHypervisorInfoForHost return results; } + /** + * Strip a leading {@code V} or {@code v} from an OS version string when the + * next character is a digit. Some distributions (notably Kylin Linux Advanced + * Server) expose {@code VERSION_ID="V10"} via {@code /etc/os-release}, while + * the matching DVD metadata script outputs the same release as a plain + * {@code 10}. Without normalization the two sides build different + * {@code osReleaseVersion} keys (e.g. {@code "kylin V10"} vs {@code "kylin 10"}) + * and the metadata join silently returns no rows, leaving + * {@code matchTargetVersion} null and the host stuck in {@code Unknown}. + * See ZSTAC-83682. + */ + public static String normalizeOsVersion(String version) { + if (version == null) { + return null; + } + String trimmed = version.trim(); + if (trimmed.length() > 1 + && (trimmed.charAt(0) == 'V' || trimmed.charAt(0) == 'v') + && Character.isDigit(trimmed.charAt(1))) { + return trimmed.substring(1); + } + return trimmed; + } + public static HypervisorVersionState isQemuVersionMatched(String v1, String v2) { if (v1 == null || v2 == null) { return HypervisorVersionState.Unknown; diff --git a/plugin/kvm/src/main/java/org/zstack/kvm/hypervisor/KvmHypervisorInfoManagerImpl.java b/plugin/kvm/src/main/java/org/zstack/kvm/hypervisor/KvmHypervisorInfoManagerImpl.java index f9501fe279e..df330171cf6 100644 --- a/plugin/kvm/src/main/java/org/zstack/kvm/hypervisor/KvmHypervisorInfoManagerImpl.java +++ b/plugin/kvm/src/main/java/org/zstack/kvm/hypervisor/KvmHypervisorInfoManagerImpl.java @@ -275,13 +275,15 @@ private boolean saveMetadataList(List definitions) @Transactional protected boolean saveHostOsCategoryList(List categoryVOS) { + if (CollectionUtils.isEmpty(categoryVOS)) { + logger.warn("no hypervisor metadata collected from DVD, skip refresh to preserve existing metadata"); + return false; + } + // refresh all metadata with current management node SQL.New(KvmHostHypervisorMetadataVO.class) .eq(KvmHostHypervisorMetadataVO_.managementNodeUuid, Platform.getManagementServerId()) .delete(); - if (CollectionUtils.isEmpty(categoryVOS)) { - return false; - } Set requestArchitectures = categoryVOS.stream() .map(HostOsCategoryVO::getArchitecture) diff --git a/plugin/loadBalancer/src/main/java/org/zstack/network/service/lb/LoadBalancerApiInterceptor.java b/plugin/loadBalancer/src/main/java/org/zstack/network/service/lb/LoadBalancerApiInterceptor.java index 0d9946d5320..f2f8271674b 100755 --- a/plugin/loadBalancer/src/main/java/org/zstack/network/service/lb/LoadBalancerApiInterceptor.java +++ b/plugin/loadBalancer/src/main/java/org/zstack/network/service/lb/LoadBalancerApiInterceptor.java @@ -33,6 +33,7 @@ import org.zstack.header.tag.SystemTagVO_; import org.zstack.header.vm.VmNicVO; import org.zstack.header.vm.VmNicVO_; +import org.zstack.network.l3.IpRangeHelper; import org.zstack.network.service.vip.VipNetworkServicesRefVO; import org.zstack.network.service.vip.VipNetworkServicesRefVO_; import org.zstack.network.service.vip.VipVO; @@ -655,6 +656,30 @@ public void run(String arg) { q = dbf.getEntityManager().createQuery(sql, String.class); q.setParameter("uuid", msg.getListenerUuid()); msg.setLoadBalancerUuid(q.getSingleResult()); + + // When the load balancer has a VIP configured, the NIC's corresponding IP must be within L3 CIDR + LoadBalancerVO lbVO = dbf.findByUuid(msg.getLoadBalancerUuid(), LoadBalancerVO.class); + for (String nicUuid : msg.getVmNicUuids()) { + VmNicVO nicVO = dbf.findByUuid(nicUuid, VmNicVO.class); + if (nicVO == null) { + continue; + } + for (UsedIpVO usedIpVO : nicVO.getUsedIps()) { + if (!IpRangeHelper.isIpOutsideL3NetworkCidr(usedIpVO.getIp(), usedIpVO.getL3NetworkUuid())) { + continue; + } + if (lbVO.getVipUuid() != null && usedIpVO.getIpVersion() == IPv6Constants.IPv4) { + throw new ApiMessageInterceptionException(argerr(ORG_ZSTACK_NETWORK_SERVICE_LB_10176, + "cannot add VM NIC[uuid:%s] with IPv4 address[%s] which is outside L3 network CIDR range to load balancer", + nicUuid, usedIpVO.getIp())); + } + if (lbVO.getIpv6VipUuid() != null && usedIpVO.getIpVersion() == IPv6Constants.IPv6) { + throw new ApiMessageInterceptionException(argerr(ORG_ZSTACK_NETWORK_SERVICE_LB_10173, + "cannot add VM NIC[uuid:%s] with IPv6 address[%s] which is outside L3 network CIDR range to load balancer", + nicUuid, usedIpVO.getIp())); + } + } + } } private boolean hasTag(APIMessage msg, PatternedSystemTag tag) { @@ -865,6 +890,14 @@ private void validate(APICreateLoadBalancerListenerMsg msg) { statusCode = LoadBalancerSystemTags.STATUS_CODE.getTokenByTag(tag, LoadBalancerSystemTags.STATUS_CODE_TOKEN); } + if (LoadBalancerSystemTags.HTTP_COMPRESS_ALGOS.isMatch(tag)) { + String compressAlgos = LoadBalancerSystemTags.HTTP_COMPRESS_ALGOS.getTokenByTag(tag, + LoadBalancerSystemTags.HTTP_COMPRESS_ALGOS_TOKEN); + if (DisableLbSupportHttpCompressAlgos.equals(compressAlgos)) { + throw new ApiMessageInterceptionException(argerr(ORG_ZSTACK_NETWORK_SERVICE_LB_10172, + "could not create the loadbalancer listener with systemTag httpCompressAlgos::disable, please remove this tag")); + } + } } if ((redirectPort != null || statusCode != null) && (httpRedirectHttps == null || HttpRedirectHttps.disable.toString().equals(httpRedirectHttps))) { @@ -1578,6 +1611,31 @@ private void validate(APIAddBackendServerToServerGroupMsg msg){ } } + // When server group has an IP version, the vmnic's corresponding IP must be within L3 CIDR + if (groupVO.getIpVersion() != null) { + for (String nicUuid : vmNicUuids) { + VmNicVO nicVO = dbf.findByUuid(nicUuid, VmNicVO.class); + if (nicVO == null) { + continue; + } + for (UsedIpVO usedIpVO : nicVO.getUsedIps()) { + if (!IpRangeHelper.isIpOutsideL3NetworkCidr(usedIpVO.getIp(), usedIpVO.getL3NetworkUuid())) { + continue; + } + if (groupVO.getIpVersion() == IPv6Constants.IPv4 && usedIpVO.getIpVersion() == IPv6Constants.IPv4) { + throw new ApiMessageInterceptionException(argerr(ORG_ZSTACK_NETWORK_SERVICE_LB_10174, + "cannot add VM NIC[uuid:%s] with IPv4 address[%s] which is outside L3 network CIDR range to server group[uuid:%s]", + nicUuid, usedIpVO.getIp(), msg.getServerGroupUuid())); + } + if (groupVO.getIpVersion() == IPv6Constants.IPv6 && usedIpVO.getIpVersion() == IPv6Constants.IPv6) { + throw new ApiMessageInterceptionException(argerr(ORG_ZSTACK_NETWORK_SERVICE_LB_10175, + "cannot add VM NIC[uuid:%s] with IPv6 address[%s] which is outside L3 network CIDR range to server group[uuid:%s]", + nicUuid, usedIpVO.getIp(), msg.getServerGroupUuid())); + } + } + } + } + canAddVmNic = true; } diff --git a/plugin/loadBalancer/src/main/java/org/zstack/network/service/lb/LoadBalancerBase.java b/plugin/loadBalancer/src/main/java/org/zstack/network/service/lb/LoadBalancerBase.java index f51ed2efb25..dadec74b6ec 100755 --- a/plugin/loadBalancer/src/main/java/org/zstack/network/service/lb/LoadBalancerBase.java +++ b/plugin/loadBalancer/src/main/java/org/zstack/network/service/lb/LoadBalancerBase.java @@ -43,6 +43,7 @@ import org.zstack.header.vm.*; import org.zstack.header.vo.ResourceVO; import org.zstack.identity.Account; +import org.zstack.network.l3.IpRangeHelper; import org.zstack.network.l3.L3NetworkManager; import org.zstack.network.service.vip.*; import org.zstack.tag.PatternedSystemTag; @@ -635,6 +636,14 @@ private void handle(APIGetCandidateVmNicsForLoadBalancerServerGroupMsg msg) { ipVersion = groupVO.getIpVersion(); } List nicVOS = f.getAttachableVmNicsForServerGroup(self, groupVO, ipVersion); + + // Filter out NICs whose primary IP is outside L3 CIDR + nicVOS = nicVOS.stream().filter(nic -> { + String nicIp = nic.getIp(); + String nicL3 = nic.getL3NetworkUuid(); + return nicIp == null || IpRangeHelper.isIpInL3NetworkCidr(nicIp, nicL3); + }).collect(Collectors.toList()); + reply.setInventories(VmNicInventory.valueOf(nicVOS)); bus.reply(msg, reply); } @@ -1046,6 +1055,13 @@ protected void scripts() { .filter(nic -> !listenerVO.getAttachedVmNics().contains(nic.getUuid())) .collect(Collectors.toList()); + // Filter out NICs whose primary IP is outside L3 CIDR + nics = nics.stream().filter(nic -> { + String nicIp = nic.getIp(); + String nicL3 = nic.getL3NetworkUuid(); + return nicIp == null || IpRangeHelper.isIpInL3NetworkCidr(nicIp, nicL3); + }).collect(Collectors.toList()); + reply.setInventories(callGetCandidateVmNicsForLoadBalancerExtensionPoint(msg, VmNicInventory.valueOf(nics))); } }.execute(); diff --git a/plugin/portForwarding/src/main/java/org/zstack/network/service/portforwarding/PortForwardingApiInterceptor.java b/plugin/portForwarding/src/main/java/org/zstack/network/service/portforwarding/PortForwardingApiInterceptor.java index 43a2a4d10e7..5316be0629b 100755 --- a/plugin/portForwarding/src/main/java/org/zstack/network/service/portforwarding/PortForwardingApiInterceptor.java +++ b/plugin/portForwarding/src/main/java/org/zstack/network/service/portforwarding/PortForwardingApiInterceptor.java @@ -19,6 +19,8 @@ import org.zstack.header.vm.VmInstanceVO_; import org.zstack.header.vm.VmNicVO; import org.zstack.header.vm.VmNicVO_; +import org.zstack.header.network.l3.UsedIpVO; +import org.zstack.network.l3.IpRangeHelper; import org.zstack.network.service.vip.*; import org.zstack.utils.VipUseForList; import org.zstack.utils.network.IPv6Constants; @@ -147,6 +149,17 @@ public VipVO call() { } catch (CloudRuntimeException e) { throw new ApiMessageInterceptionException(operr(ORG_ZSTACK_NETWORK_SERVICE_PORTFORWARDING_10011, e.getMessage())); } + + // Check if the NIC's IP is outside L3 CIDR range + VmNicVO nicVO = dbf.findByUuid(msg.getVmNicUuid(), VmNicVO.class); + if (nicVO != null && nicVO.getUsedIpUuid() != null) { + UsedIpVO usedIpVO = dbf.findByUuid(nicVO.getUsedIpUuid(), UsedIpVO.class); + if (usedIpVO != null && IpRangeHelper.isIpOutsideL3NetworkCidr(usedIpVO.getIp(), usedIpVO.getL3NetworkUuid())) { + throw new ApiMessageInterceptionException(argerr(ORG_ZSTACK_NETWORK_SERVICE_PORTFORWARDING_10025, + "cannot bind port forwarding rule to IP address[%s] which is outside L3 network CIDR range", + usedIpVO.getIp())); + } + } } private boolean rangeOverlap(int s1, int e1, int s2, int e2) { @@ -243,6 +256,17 @@ private void validate(APICreatePortForwardingRuleMsg msg) { checkIfAnotherVip(msg.getVipUuid(), msg.getVmNicUuid()); checkForConflictsWithOtherRules(msg.getVmNicUuid(), msg.getPrivatePortStart(), msg.getPrivatePortEnd(), msg.getAllowedCidr(), PortForwardingProtocolType.valueOf(msg.getProtocolType())); + + // Check if the NIC's IP is outside L3 CIDR range + VmNicVO nicVO = dbf.findByUuid(msg.getVmNicUuid(), VmNicVO.class); + if (nicVO != null && nicVO.getUsedIpUuid() != null) { + UsedIpVO usedIpVO = dbf.findByUuid(nicVO.getUsedIpUuid(), UsedIpVO.class); + if (usedIpVO != null && IpRangeHelper.isIpOutsideL3NetworkCidr(usedIpVO.getIp(), usedIpVO.getL3NetworkUuid())) { + throw new ApiMessageInterceptionException(argerr(ORG_ZSTACK_NETWORK_SERVICE_PORTFORWARDING_10025, + "cannot bind port forwarding rule to IP address[%s] which is outside L3 network CIDR range", + usedIpVO.getIp())); + } + } } if(msg.getAllowedCidr() != null){ diff --git a/plugin/portForwarding/src/main/java/org/zstack/network/service/portforwarding/PortForwardingManagerImpl.java b/plugin/portForwarding/src/main/java/org/zstack/network/service/portforwarding/PortForwardingManagerImpl.java index ce34e098021..80aa7597ff1 100755 --- a/plugin/portForwarding/src/main/java/org/zstack/network/service/portforwarding/PortForwardingManagerImpl.java +++ b/plugin/portForwarding/src/main/java/org/zstack/network/service/portforwarding/PortForwardingManagerImpl.java @@ -37,6 +37,7 @@ import org.zstack.header.query.ExpandedQueryStruct; import org.zstack.header.vm.*; import org.zstack.identity.AccountManager; +import org.zstack.network.l3.IpRangeHelper; import org.zstack.network.l3.L3NetworkManager; import org.zstack.network.service.NetworkServiceManager; import org.zstack.network.service.vip.*; @@ -365,7 +366,16 @@ protected List scripts() { /* TODO: only ipv4 portforwarding is supported */ List nicInvs = VmNicInventory.valueOf(nics.stream().filter(nic -> !usedVm.contains(nic.getVmInstanceUuid())).collect(Collectors.toList())); - return l3Mgr.filterVmNicByIpVersion(nicInvs, IPv6Constants.IPv4); + List filtered = l3Mgr.filterVmNicByIpVersion(nicInvs, IPv6Constants.IPv4); + + // Filter out NICs whose primary IP is outside L3 CIDR + filtered = filtered.stream().filter(nic -> { + String nicIp = nic.getIp(); + String nicL3 = nic.getL3NetworkUuid(); + return nicIp == null || IpRangeHelper.isIpInL3NetworkCidr(nicIp, nicL3); + }).collect(Collectors.toList()); + + return filtered; } }.execute(); } diff --git a/plugin/securityGroup/src/main/java/org/zstack/network/securitygroup/SecurityGroupApiInterceptor.java b/plugin/securityGroup/src/main/java/org/zstack/network/securitygroup/SecurityGroupApiInterceptor.java index 135ad3886aa..d76b88610b1 100755 --- a/plugin/securityGroup/src/main/java/org/zstack/network/securitygroup/SecurityGroupApiInterceptor.java +++ b/plugin/securityGroup/src/main/java/org/zstack/network/securitygroup/SecurityGroupApiInterceptor.java @@ -343,14 +343,6 @@ private void validate(APISetVmNicSecurityGroupMsg msg) { if (!aoMap.isEmpty()) { Integer[] priorities = aoMap.keySet().toArray(new Integer[aoMap.size()]); Arrays.sort(priorities); - if (priorities[0] != 1) { - throw new ApiMessageInterceptionException(argerr(ORG_ZSTACK_NETWORK_SECURITYGROUP_10022, "could no set vm nic security group, because invalid priority, priority expects to start at 1, but [%d]", priorities[0])); - } - for (int i = 0; i < priorities.length - 1; i++) { - if (priorities[i] + 1 != priorities[i + 1]) { - throw new ApiMessageInterceptionException(argerr(ORG_ZSTACK_NETWORK_SECURITYGROUP_10023, "could no set vm nic security group, because invalid priority, priority[%d] and priority[%d] expected to be consecutive", priorities[i], priorities[i + 1])); - } - } } @@ -386,19 +378,6 @@ private void validate(APISetVmNicSecurityGroupMsg msg) { msg.setRefs(newAOs); } - } else { - if (!adminIntegers.isEmpty()) { - Integer[] priorities = adminIntegers.toArray(new Integer[adminIntegers.size()]); - Arrays.sort(priorities); - if (priorities[0] != 1) { - throw new ApiMessageInterceptionException(argerr(ORG_ZSTACK_NETWORK_SECURITYGROUP_10024, "could no set vm nic security group, because admin security group priority[%d] must be higher than users", priorities[0])); - } - for (int i = 0; i < priorities.length - 1; i++) { - if (priorities[i] + 1 != priorities[i + 1]) { - throw new ApiMessageInterceptionException(argerr(ORG_ZSTACK_NETWORK_SECURITYGROUP_10025, "could no set vm nic security group, because admin security group priority[%d] must be higher than users", priorities[i + 1])); - } - } - } } } @@ -498,8 +477,9 @@ private void validate(APIUpdateSecurityGroupRulePriorityMsg msg) { rvos.stream().filter(rvo -> rvo.getUuid().equals(ao.getRuleUuid())).findFirst().orElseThrow(() -> new ApiMessageInterceptionException(argerr(ORG_ZSTACK_NETWORK_SECURITYGROUP_10041, "could not update security group rule priority, because rule[uuid:%s] not in security group[uuid:%s]", ao.getRuleUuid(), msg.getSecurityGroupUuid()))); - rvos.stream().filter(rvo -> rvo.getPriority() == ao.getPriority()).findFirst().orElseThrow(() -> - new ApiMessageInterceptionException(argerr(ORG_ZSTACK_NETWORK_SECURITYGROUP_10042, "could not update security group rule priority, because priority[%d] not in security group[uuid:%s]", ao.getPriority(), msg.getSecurityGroupUuid()))); + if (ao.getPriority() < 1 || ao.getPriority() > SecurityGroupGlobalConfig.SECURITY_GROUP_RULES_NUM_LIMIT.value(Integer.class)) { + throw new ApiMessageInterceptionException(argerr(ORG_ZSTACK_NETWORK_SECURITYGROUP_10042, "could not update security group rule priority, because priority[%d] is out of valid range [1, %d]", ao.getPriority(), SecurityGroupGlobalConfig.SECURITY_GROUP_RULES_NUM_LIMIT.value(Integer.class))); + } } List uuidList = new ArrayList<>(priorityMap.values()); @@ -534,8 +514,8 @@ private void validate(APIChangeSecurityGroupRuleMsg msg) { if (count.intValue() > SecurityGroupGlobalConfig.SECURITY_GROUP_RULES_NUM_LIMIT.value(Integer.class)) { throw new ApiMessageInterceptionException(argerr(ORG_ZSTACK_NETWORK_SECURITYGROUP_10047, "could not change security group rule, because security group %s rules number[%d] is out of max limit[%d]", vo.getType(), count.intValue(), SecurityGroupGlobalConfig.SECURITY_GROUP_RULES_NUM_LIMIT.value(Integer.class))); } - if (msg.getPriority() > count.intValue()) { - throw new ApiMessageInterceptionException(argerr(ORG_ZSTACK_NETWORK_SECURITYGROUP_10048, "could not change security group rule, because the maximum priority of %s rule is [%d]", vo.getType().toString(), count.intValue())); + if (msg.getPriority() > SecurityGroupGlobalConfig.SECURITY_GROUP_RULES_NUM_LIMIT.value(Integer.class)) { + throw new ApiMessageInterceptionException(argerr(ORG_ZSTACK_NETWORK_SECURITYGROUP_10048, "could not change security group rule, because the maximum priority of %s rule is [%d]", vo.getType().toString(), SecurityGroupGlobalConfig.SECURITY_GROUP_RULES_NUM_LIMIT.value(Integer.class))); } if (msg.getPriority() < 0) { msg.setPriority(SecurityGroupConstant.LOWEST_RULE_PRIORITY); @@ -1198,11 +1178,11 @@ private void validate(APIAddSecurityGroupRuleMsg msg) { throw new ApiMessageInterceptionException(argerr(ORG_ZSTACK_NETWORK_SECURITYGROUP_10119, "could not add security group rule, because security group %s rules number[%d] is out of max limit[%d]", SecurityGroupRuleType.Egress, (egressRuleCount + toCreateEgressRuleCount), SecurityGroupGlobalConfig.SECURITY_GROUP_RULES_NUM_LIMIT.value(Integer.class))); } - if (msg.getPriority() > (ingressRuleCount + 1) && toCreateIngressRuleCount > 0) { - throw new ApiMessageInterceptionException(argerr(ORG_ZSTACK_NETWORK_SECURITYGROUP_10120, "could not add security group rule, because priority[%d] must be consecutive, the ingress rule maximum priority is [%d]", msg.getPriority(), ingressRuleCount)); + if (msg.getPriority() > SecurityGroupGlobalConfig.SECURITY_GROUP_RULES_NUM_LIMIT.value(Integer.class) && toCreateIngressRuleCount > 0) { + throw new ApiMessageInterceptionException(argerr(ORG_ZSTACK_NETWORK_SECURITYGROUP_10120, "could not add security group rule, because priority[%d] exceeds the maximum allowed priority [%d]", msg.getPriority(), SecurityGroupGlobalConfig.SECURITY_GROUP_RULES_NUM_LIMIT.value(Integer.class))); } - if (msg.getPriority() > (egressRuleCount + 1) && toCreateEgressRuleCount > 0) { - throw new ApiMessageInterceptionException(argerr(ORG_ZSTACK_NETWORK_SECURITYGROUP_10121, "could not add security group rule, because priority[%d] must be consecutive, the egress rule maximum priority is [%d]", msg.getPriority(), egressRuleCount)); + if (msg.getPriority() > SecurityGroupGlobalConfig.SECURITY_GROUP_RULES_NUM_LIMIT.value(Integer.class) && toCreateEgressRuleCount > 0) { + throw new ApiMessageInterceptionException(argerr(ORG_ZSTACK_NETWORK_SECURITYGROUP_10121, "could not add security group rule, because priority[%d] exceeds the maximum allowed priority [%d]", msg.getPriority(), SecurityGroupGlobalConfig.SECURITY_GROUP_RULES_NUM_LIMIT.value(Integer.class))); } } diff --git a/plugin/virtualRouterProvider/src/main/java/org/zstack/network/service/virtualrouter/VirtualRouterManagerImpl.java b/plugin/virtualRouterProvider/src/main/java/org/zstack/network/service/virtualrouter/VirtualRouterManagerImpl.java index 610331e5f6c..a876ee24a34 100755 --- a/plugin/virtualRouterProvider/src/main/java/org/zstack/network/service/virtualrouter/VirtualRouterManagerImpl.java +++ b/plugin/virtualRouterProvider/src/main/java/org/zstack/network/service/virtualrouter/VirtualRouterManagerImpl.java @@ -2092,6 +2092,9 @@ void applianceVmsDeleteIpByIpRanges(List applianceVmVOS, vo = dbf.findByUuid(vo.getUuid(), ApplianceVmVO.class); for (VmNicVO nic : vo.getVmNics()) { for (UsedIpVO ip : nic.getUsedIps()) { + if (ip.getIpRangeUuid() == null) { + continue; + } if (ip.getIpVersion() == IPv6Constants.IPv4 && ipv4RangeUuids.contains(ip.getIpRangeUuid())) { ReturnIpMsg rmsg = new ReturnIpMsg(); rmsg.setL3NetworkUuid(ip.getL3NetworkUuid()); @@ -2139,7 +2142,7 @@ public List applianceVmsToDeleteNicByIpRanges(List appli for (ApplianceVmVO vo : applianceVmVOS) { for (VmNicVO nic : vo.getVmNics()) { for (UsedIpVO ip : nic.getUsedIps()) { - if (!iprUuids.contains(ip.getIpRangeUuid())) { + if (ip.getIpRangeUuid() == null || !iprUuids.contains(ip.getIpRangeUuid())) { continue; } @@ -2172,7 +2175,7 @@ public List applianceVmsToBeDeletedByIpRanges(List } /* if any ip of the nic is deleted, delete the appliance vm */ - if (nic.getUsedIps().stream().anyMatch(ip -> iprUuids.contains(ip.getIpRangeUuid()))) { + if (nic.getUsedIps().stream().anyMatch(ip -> ip.getIpRangeUuid() != null && iprUuids.contains(ip.getIpRangeUuid()))) { toDeleted.add(vos); break; } diff --git a/plugin/xinfini/src/main/java/org/zstack/xinfini/XInfiniStorageFactory.java b/plugin/xinfini/src/main/java/org/zstack/xinfini/XInfiniStorageFactory.java index 70d33ea08d0..c3f98670bb6 100644 --- a/plugin/xinfini/src/main/java/org/zstack/xinfini/XInfiniStorageFactory.java +++ b/plugin/xinfini/src/main/java/org/zstack/xinfini/XInfiniStorageFactory.java @@ -11,6 +11,7 @@ import org.zstack.header.xinfini.XInfiniConstants; import org.zstack.storage.addon.primary.ExternalPrimaryStorageFactory; +import java.util.ArrayList; import java.util.LinkedHashMap; import java.util.List; @@ -55,7 +56,7 @@ public String getIdentity() { @Override public List getPreferBackupStorageTypes() { - return preferBackupStorageTypes; + return new ArrayList<>(preferBackupStorageTypes); } public void setPreferBackupStorageTypes(List preferBackupStorageTypes) { diff --git a/plugin/zbs/src/main/java/org/zstack/storage/zbs/ZbsStorageFactory.java b/plugin/zbs/src/main/java/org/zstack/storage/zbs/ZbsStorageFactory.java index 5b7c491814e..9df6cd4c97a 100644 --- a/plugin/zbs/src/main/java/org/zstack/storage/zbs/ZbsStorageFactory.java +++ b/plugin/zbs/src/main/java/org/zstack/storage/zbs/ZbsStorageFactory.java @@ -11,6 +11,7 @@ import org.zstack.utils.ssh.Ssh; import org.zstack.utils.ssh.SshResult; +import java.util.ArrayList; import java.util.List; import static org.zstack.core.Platform.operr; @@ -93,7 +94,7 @@ public void setPreferBackupStorageTypes(List preferBackupStorageTypes) { @Override public List getPreferBackupStorageTypes() { - return preferBackupStorageTypes; + return new ArrayList<>(preferBackupStorageTypes); } @Override diff --git a/rest/src/main/java/org/zstack/rest/RestServer.java b/rest/src/main/java/org/zstack/rest/RestServer.java index 917ffaadcdf..ee3dbb0b355 100755 --- a/rest/src/main/java/org/zstack/rest/RestServer.java +++ b/rest/src/main/java/org/zstack/rest/RestServer.java @@ -408,8 +408,11 @@ private void callWebHook(RequestData d) throws IllegalAccessException, NoSuchMet writeResponse(response, w, ret.getResult()); } else { + // localize with webhook caller's locale (message already populated by Platform.err) String locale = resolveLocale(); - i18nService.localizeErrorCode(evt.getError(), locale); + if (!LocaleUtils.DEFAULT_LOCALE.equals(locale)) { + i18nService.localizeErrorCode(evt.getError(), locale); + } response.setError(evt.getError()); } @@ -917,14 +920,18 @@ private void handleJobQuery(HttpServletRequest req, HttpServletResponse rsp) thr writeResponse(response, w, ret.getResult()); sendResponse(HttpStatus.OK.value(), response, rsp); } else { - String locale = resolveLocaleFromRequest(req); - i18nService.localizeErrorCode(evt.getError(), locale); response.setError(evt.getError()); sendResponse(HttpStatus.SERVICE_UNAVAILABLE.value(), response, rsp); } } private void sendResponse(int statusCode, ApiResponse response, HttpServletResponse rsp) throws IOException { + // centralized localization: override message with client's preferred locale + if (response.getError() != null) { + String locale = resolveLocale(); + i18nService.localizeErrorCode(response.getError(), locale); + } + RequestInfo info = requestInfo.get(); if (requestLogger.isTraceEnabled() && needLog(info)) { String body = CloudBusGson.toJson(response); @@ -1428,11 +1435,6 @@ private String resolveLocale() { return LocaleUtils.resolveLocale(acceptLanguage, i18nService.getAvailableLocales()); } - private String resolveLocaleFromRequest(HttpServletRequest req) { - String acceptLanguage = req.getHeader("Accept-Language"); - return LocaleUtils.resolveLocale(acceptLanguage, i18nService.getAvailableLocales()); - } - private void sendReplyResponse(MessageReply reply, Api api, HttpServletResponse rsp) throws IOException, IllegalAccessException, NoSuchMethodException, InvocationTargetException { ApiResponse response = new ApiResponse(); @@ -1440,6 +1442,9 @@ private void sendReplyResponse(MessageReply reply, Api api, HttpServletResponse String locale = resolveLocale(); i18nService.localizeErrorCode(reply.getError(), locale); response.setError(reply.getError()); + // use JSONObjectUtil (which disables HTML escaping) to keep the same + // serialization behavior as before; CloudBusGson.httpGson escapes '\'' to + // '\u0027' which breaks SDK-side string assertions (ZSTAC-71075 etc.) sendResponse(HttpStatus.SERVICE_UNAVAILABLE.value(), JSONObjectUtil.toJsonString(response), rsp); return; } diff --git a/runMavenProfile b/runMavenProfile index c911240dee3..5415b97953a 100755 --- a/runMavenProfile +++ b/runMavenProfile @@ -1,5 +1,14 @@ #!/bin/bash +# Auto-detect isolated .m2 in worktree +SCRIPT_DIR="$(dirname "$(readlink -f "$0")")" +if [ -d "$SCRIPT_DIR/.m2/repository" ]; then + MVN_LOCAL="-Dmaven.repo.local=$SCRIPT_DIR/.m2/repository" + echo "[runMavenProfile] Using isolated .m2: $SCRIPT_DIR/.m2/repository" +else + MVN_LOCAL="" +fi + usage() { echo "Command line tool of zstack project @@ -65,7 +74,7 @@ Options: " } -MVNTest="mvn test -Djacoco.skip=true" +MVNTest="mvn test -Djacoco.skip=true $MVN_LOCAL" py() { if [ -d premium/test-premium ]; then @@ -111,7 +120,7 @@ openapi() { fi cd tool/doclet - mvn -Dmaven.test.skip=true package + mvn -Dmaven.test.skip=true package $MVN_LOCAL cd - javadoc -private -doclet org.zstack.tool.doclet.JsonDocLet -docletpath tool/doclet/target/doclet-*-jar-with-dependencies.jar $(find -name *.java) @@ -172,7 +181,7 @@ mdpremium() { errorcode() { cd test - mvn test -Dtest=TestGenerateErrorCodeDoc + mvn test -Dtest=TestGenerateErrorCodeDoc $MVN_LOCAL cd - >/dev/null } @@ -205,7 +214,7 @@ triggerexpression() { fi cd premium/mevoco - mvn -P trigger-expression generate-sources + mvn -P trigger-expression generate-sources $MVN_LOCAL mkdir -p src/main/java/org/zstack/monitoring/trigger/expression/antlr4 yes | cp target/generated-sources/antlr4/TriggerExpression* src/main/java/org/zstack/monitoring/trigger/expression/antlr4 rm -f target/generated-sources/antlr4/TriggerExpression* @@ -214,14 +223,14 @@ triggerexpression() { zql() { cd search - mvn -P zql generate-sources + mvn -P zql generate-sources $MVN_LOCAL if [ $? -ne 0 ]; then exit 1 fi mkdir -p src/main/java/org/zstack/zql/antlr4/ yes | cp target/generated-sources/antlr4/* src/main/java/org/zstack/zql/antlr4/ rm -f target/generated-sources/antlr4/* - mvn -Dmaven.test.skip=true clean install + mvn -Dmaven.test.skip=true clean install $MVN_LOCAL cd - >/dev/null } @@ -232,7 +241,7 @@ cloudwatchfunction() { fi cd premium/zwatch - mvn -P function generate-sources + mvn -P function generate-sources $MVN_LOCAL mkdir -p src/main/java/org/zstack/zwatch/api/antlr4/ yes | cp target/generated-sources/antlr4/MetricFunction* src/main/java/org/zstack/zwatch/api/antlr4/ rm -f target/generated-sources/antlr4/MetricFunction* @@ -246,7 +255,7 @@ zwatchzql() { fi cd premium/zwatch - mvn -P function generate-sources + mvn -P function generate-sources $MVN_LOCAL mkdir -p src/main/java/org/zstack/zwatch/returnwith/antlr4/ yes | cp target/generated-sources/antlr4/ReturnWith* src/main/java/org/zstack/zwatch/returnwith/antlr4/ rm -f target/generated-sources/antlr4/ReturnWith* @@ -499,7 +508,7 @@ clear_git_config() { run_profile() { if test x$1 = x'premium'; then - mvn -Dmaven.test.skip=true -P premium clean install + mvn -Dmaven.test.skip=true -P premium clean install $MVN_LOCAL elif test x$1 = x'md'; then md elif test x$1 = x'sdk'; then @@ -549,7 +558,7 @@ run_profile() { elif test x$1 = x'gosdk'; then gosdk else - mvn -pl build -P $1 exec:exec -D$1 + mvn -pl build -P $1 exec:exec -D$1 $MVN_LOCAL fi } diff --git a/sdk/src/main/java/SourceClassMap.java b/sdk/src/main/java/SourceClassMap.java index 4bbd9238f98..6decef8aaec 100644 --- a/sdk/src/main/java/SourceClassMap.java +++ b/sdk/src/main/java/SourceClassMap.java @@ -9,6 +9,7 @@ public class SourceClassMap { put("org.zstack.abstraction.OptionType$InputType", "org.zstack.sdk.InputType"); put("org.zstack.accessKey.AccessKeyInventory", "org.zstack.sdk.AccessKeyInventory"); put("org.zstack.accessKey.AccessKeyState", "org.zstack.sdk.AccessKeyState"); + put("org.zstack.accessKey.AccessKeyType", "org.zstack.sdk.AccessKeyType"); put("org.zstack.ai.NginxRedirectRule", "org.zstack.sdk.NginxRedirectRule"); put("org.zstack.ai.entity.ApplicationDevelopmentServiceInventory", "org.zstack.sdk.ApplicationDevelopmentServiceInventory"); put("org.zstack.ai.entity.DatasetInventory", "org.zstack.sdk.DatasetInventory"); @@ -24,6 +25,8 @@ public class SourceClassMap { put("org.zstack.ai.entity.ModelServiceRefInventory", "org.zstack.sdk.ModelServiceRefInventory"); put("org.zstack.ai.entity.ModelServiceTemplateInventory", "org.zstack.sdk.ModelServiceTemplateInventory"); put("org.zstack.ai.entity.TrainedModelRecordInventory", "org.zstack.sdk.TrainedModelRecordInventory"); + put("org.zstack.ai.entity.VmModelMountInventory", "org.zstack.sdk.VmModelMountInventory"); + put("org.zstack.ai.entity.VmModelMountStatus", "org.zstack.sdk.VmModelMountStatus"); put("org.zstack.ai.message.ArchitectureImageMapping", "org.zstack.sdk.ArchitectureImageMapping"); put("org.zstack.ai.message.MaaSUsage", "org.zstack.sdk.MaaSUsage"); put("org.zstack.ai.message.ModelCenterServiceInventory", "org.zstack.sdk.ModelCenterServiceInventory"); @@ -65,8 +68,11 @@ public class SourceClassMap { put("org.zstack.baremetal2.chassis.BareMetal2ChassisInventory", "org.zstack.sdk.BareMetal2ChassisInventory"); put("org.zstack.baremetal2.chassis.BareMetal2ChassisNicInventory", "org.zstack.sdk.BareMetal2ChassisNicInventory"); put("org.zstack.baremetal2.chassis.BareMetal2ChassisPciDeviceInventory", "org.zstack.sdk.BareMetal2ChassisPciDeviceInventory"); + put("org.zstack.baremetal2.chassis.dpu.BareMetal2DpuChassisConfig", "org.zstack.sdk.BareMetal2DpuChassisConfig"); + put("org.zstack.baremetal2.chassis.dpu.BareMetal2DpuChassisInventory", "org.zstack.sdk.BareMetal2DpuChassisInventory"); put("org.zstack.baremetal2.chassis.ipmi.BareMetal2IpmiChassisInventory", "org.zstack.sdk.BareMetal2IpmiChassisInventory"); put("org.zstack.baremetal2.configuration.BareMetal2ChassisOfferingInventory", "org.zstack.sdk.BareMetal2ChassisOfferingInventory"); + put("org.zstack.baremetal2.dpu.BareMetal2DpuHostInventory", "org.zstack.sdk.BareMetal2DpuHostInventory"); put("org.zstack.baremetal2.gateway.BareMetal2GatewayInventory", "org.zstack.sdk.BareMetal2GatewayInventory"); put("org.zstack.baremetal2.gateway.BareMetal2GatewayProvisionNicInventory", "org.zstack.sdk.BareMetal2GatewayProvisionNicInventory"); put("org.zstack.baremetal2.instance.BareMetal2InstanceInventory", "org.zstack.sdk.BareMetal2InstanceInventory"); @@ -244,8 +250,10 @@ public class SourceClassMap { put("org.zstack.header.console.ConsoleProxyAgentInventory", "org.zstack.sdk.ConsoleProxyAgentInventory"); put("org.zstack.header.core.external.plugin.PluginDriverInventory", "org.zstack.sdk.PluginDriverInventory"); put("org.zstack.header.core.external.service.ExternalServiceCapabilities", "org.zstack.sdk.ExternalServiceCapabilities"); + put("org.zstack.header.core.external.service.ExternalServiceConfigurationInventory", "org.zstack.sdk.ExternalServiceConfigurationInventory"); put("org.zstack.header.core.external.service.ExternalServiceInventory", "org.zstack.sdk.ExternalServiceInventory"); put("org.zstack.header.core.progress.ChainInfo", "org.zstack.sdk.ChainInfo"); + put("org.zstack.header.core.progress.LongJobProgressDetail", "org.zstack.sdk.LongJobProgressDetail"); put("org.zstack.header.core.progress.PendingTaskInfo", "org.zstack.sdk.PendingTaskInfo"); put("org.zstack.header.core.progress.RunningTaskInfo", "org.zstack.sdk.RunningTaskInfo"); put("org.zstack.header.core.progress.TaskInfo", "org.zstack.sdk.TaskInfo"); @@ -635,8 +643,13 @@ public class SourceClassMap { put("org.zstack.pciDevice.PciDeviceType", "org.zstack.sdk.PciDeviceType"); put("org.zstack.pciDevice.gpu.GpuAllocateStatus", "org.zstack.sdk.GpuAllocateStatus"); put("org.zstack.pciDevice.gpu.GpuDeviceInventory", "org.zstack.sdk.GpuDeviceInventory"); + put("org.zstack.pciDevice.gpu.GpuDeviceSpecCandidateInventory", "org.zstack.sdk.GpuDeviceSpecCandidateInventory"); put("org.zstack.pciDevice.gpu.GpuDeviceSpecInventory", "org.zstack.sdk.GpuDeviceSpecInventory"); put("org.zstack.pciDevice.gpu.GpuVendor", "org.zstack.sdk.GpuVendor"); + put("org.zstack.pciDevice.gpu.dgpu.DGpuDeviceInventory", "org.zstack.sdk.DGpuDeviceInventory"); + put("org.zstack.pciDevice.gpu.dgpu.DGpuProfileInventory", "org.zstack.sdk.DGpuProfileInventory"); + put("org.zstack.pciDevice.gpu.dgpu.DGpuSpecStatsInventory", "org.zstack.sdk.DGpuSpecStatsInventory"); + put("org.zstack.pciDevice.gpu.dgpu.DGpuStatus", "org.zstack.sdk.DGpuStatus"); put("org.zstack.pciDevice.specification.mdev.MdevDeviceSpecInventory", "org.zstack.sdk.MdevDeviceSpecInventory"); put("org.zstack.pciDevice.specification.mdev.MdevDeviceSpecState", "org.zstack.sdk.MdevDeviceSpecState"); put("org.zstack.pciDevice.specification.mdev.PciDeviceMdevSpecRefInventory", "org.zstack.sdk.PciDeviceMdevSpecRefInventory"); @@ -644,6 +657,8 @@ public class SourceClassMap { put("org.zstack.pciDevice.specification.pci.PciDeviceSpecInventory", "org.zstack.sdk.PciDeviceSpecInventory"); put("org.zstack.pciDevice.specification.pci.PciDeviceSpecState", "org.zstack.sdk.PciDeviceSpecState"); put("org.zstack.pciDevice.specification.pci.VmInstancePciDeviceSpecRefInventory", "org.zstack.sdk.VmInstancePciDeviceSpecRefInventory"); + put("org.zstack.pciDevice.virtual.PciDeviceVirtMode", "org.zstack.sdk.PciDeviceVirtMode"); + put("org.zstack.pciDevice.virtual.PciDeviceVirtState", "org.zstack.sdk.PciDeviceVirtState"); put("org.zstack.pciDevice.virtual.PciDeviceVirtStatus", "org.zstack.sdk.PciDeviceVirtStatus"); put("org.zstack.pciDevice.virtual.vfio_mdev.MdevDeviceChooser", "org.zstack.sdk.MdevDeviceChooser"); put("org.zstack.pciDevice.virtual.vfio_mdev.MdevDeviceInventory", "org.zstack.sdk.MdevDeviceInventory"); @@ -870,6 +885,10 @@ public class SourceClassMap { put("org.zstack.zwatch.monitorgroup.entity.MonitorGroupTemplateRefInventory", "org.zstack.sdk.zwatch.monitorgroup.entity.MonitorGroupTemplateRefInventory"); put("org.zstack.zwatch.monitorgroup.entity.MonitorGroupTemplateRefVO", "org.zstack.sdk.zwatch.monitorgroup.entity.MonitorGroupTemplateRefVO"); put("org.zstack.zwatch.monitorgroup.entity.MonitorTemplateInventory", "org.zstack.sdk.zwatch.monitorgroup.entity.MonitorTemplateInventory"); + put("org.zstack.zwatch.resnotify.ResNotifySubscriptionInventory", "org.zstack.sdk.zwatch.resnotify.ResNotifySubscriptionInventory"); + put("org.zstack.zwatch.resnotify.ResNotifySubscriptionState", "org.zstack.sdk.zwatch.resnotify.ResNotifySubscriptionState"); + put("org.zstack.zwatch.resnotify.ResNotifyType", "org.zstack.sdk.zwatch.resnotify.ResNotifyType"); + put("org.zstack.zwatch.resnotify.ResNotifyWebhookRefInventory", "org.zstack.sdk.zwatch.resnotify.ResNotifyWebhookRefInventory"); put("org.zstack.zwatch.ruleengine.ComparisonOperator", "org.zstack.sdk.zwatch.ruleengine.ComparisonOperator"); put("org.zstack.zwatch.thirdparty.entity.SNSEndpointThirdpartyAlertHistoryInventory", "org.zstack.sdk.zwatch.thirdparty.entity.SNSEndpointThirdpartyAlertHistoryInventory"); put("org.zstack.zwatch.thirdparty.entity.ThirdpartyOriginalAlertInventory", "org.zstack.sdk.zwatch.thirdparty.entity.ThirdpartyOriginalAlertInventory"); @@ -884,6 +903,7 @@ public class SourceClassMap { put("org.zstack.sdk.AccessControlRuleInventory", "org.zstack.loginControl.entity.AccessControlRuleInventory"); put("org.zstack.sdk.AccessKeyInventory", "org.zstack.accessKey.AccessKeyInventory"); put("org.zstack.sdk.AccessKeyState", "org.zstack.accessKey.AccessKeyState"); + put("org.zstack.sdk.AccessKeyType", "org.zstack.accessKey.AccessKeyType"); put("org.zstack.sdk.AccessPathInfo", "org.zstack.header.volume.block.AccessPathInfo"); put("org.zstack.sdk.AccountInventory", "org.zstack.header.identity.AccountInventory"); put("org.zstack.sdk.AccountPriceTableRefInventory", "org.zstack.billing.table.AccountPriceTableRefInventory"); @@ -950,6 +970,9 @@ public class SourceClassMap { put("org.zstack.sdk.BareMetal2ChassisNicInventory", "org.zstack.baremetal2.chassis.BareMetal2ChassisNicInventory"); put("org.zstack.sdk.BareMetal2ChassisOfferingInventory", "org.zstack.baremetal2.configuration.BareMetal2ChassisOfferingInventory"); put("org.zstack.sdk.BareMetal2ChassisPciDeviceInventory", "org.zstack.baremetal2.chassis.BareMetal2ChassisPciDeviceInventory"); + put("org.zstack.sdk.BareMetal2DpuChassisConfig", "org.zstack.baremetal2.chassis.dpu.BareMetal2DpuChassisConfig"); + put("org.zstack.sdk.BareMetal2DpuChassisInventory", "org.zstack.baremetal2.chassis.dpu.BareMetal2DpuChassisInventory"); + put("org.zstack.sdk.BareMetal2DpuHostInventory", "org.zstack.baremetal2.dpu.BareMetal2DpuHostInventory"); put("org.zstack.sdk.BareMetal2GatewayInventory", "org.zstack.baremetal2.gateway.BareMetal2GatewayInventory"); put("org.zstack.sdk.BareMetal2GatewayProvisionNicInventory", "org.zstack.baremetal2.gateway.BareMetal2GatewayProvisionNicInventory"); put("org.zstack.sdk.BareMetal2InstanceInventory", "org.zstack.baremetal2.instance.BareMetal2InstanceInventory"); @@ -1022,6 +1045,10 @@ public class SourceClassMap { put("org.zstack.sdk.CpuMemoryCapacityData", "org.zstack.header.allocator.datatypes.CpuMemoryCapacityData"); put("org.zstack.sdk.CreateDataVolumeTemplateFromVolumeSnapshotFailure", "org.zstack.header.image.APICreateDataVolumeTemplateFromVolumeSnapshotEvent$Failure"); put("org.zstack.sdk.CreateRootVolumeTemplateFromVolumeSnapshotFailure", "org.zstack.header.image.APICreateRootVolumeTemplateFromVolumeSnapshotEvent$Failure"); + put("org.zstack.sdk.DGpuDeviceInventory", "org.zstack.pciDevice.gpu.dgpu.DGpuDeviceInventory"); + put("org.zstack.sdk.DGpuProfileInventory", "org.zstack.pciDevice.gpu.dgpu.DGpuProfileInventory"); + put("org.zstack.sdk.DGpuSpecStatsInventory", "org.zstack.pciDevice.gpu.dgpu.DGpuSpecStatsInventory"); + put("org.zstack.sdk.DGpuStatus", "org.zstack.pciDevice.gpu.dgpu.DGpuStatus"); put("org.zstack.sdk.DRSAdviceInventory", "org.zstack.drs.entity.DRSAdviceInventory"); put("org.zstack.sdk.DRSVmMigrationActivityInventory", "org.zstack.drs.entity.DRSVmMigrationActivityInventory"); put("org.zstack.sdk.DataCenterInventory", "org.zstack.header.datacenter.DataCenterInventory"); @@ -1062,6 +1089,7 @@ public class SourceClassMap { put("org.zstack.sdk.ExternalPrimaryStorageInventory", "org.zstack.header.storage.addon.primary.ExternalPrimaryStorageInventory"); put("org.zstack.sdk.ExternalServiceCapabilities", "org.zstack.header.core.external.service.ExternalServiceCapabilities"); put("org.zstack.sdk.ExternalServiceCapabilitiesBuilder", "org.zstack.core.externalservice.ExternalServiceCapabilitiesBuilder"); + put("org.zstack.sdk.ExternalServiceConfigurationInventory", "org.zstack.header.core.external.service.ExternalServiceConfigurationInventory"); put("org.zstack.sdk.ExternalServiceInventory", "org.zstack.header.core.external.service.ExternalServiceInventory"); put("org.zstack.sdk.FaultToleranceVmGroupInventory", "org.zstack.faulttolerance.entity.FaultToleranceVmGroupInventory"); put("org.zstack.sdk.FcHbaDeviceInventory", "org.zstack.storage.device.hba.FcHbaDeviceInventory"); @@ -1083,6 +1111,7 @@ public class SourceClassMap { put("org.zstack.sdk.GlobalConfigTemplateInventory", "org.zstack.templateConfig.GlobalConfigTemplateInventory"); put("org.zstack.sdk.GpuAllocateStatus", "org.zstack.pciDevice.gpu.GpuAllocateStatus"); put("org.zstack.sdk.GpuDeviceInventory", "org.zstack.pciDevice.gpu.GpuDeviceInventory"); + put("org.zstack.sdk.GpuDeviceSpecCandidateInventory", "org.zstack.pciDevice.gpu.GpuDeviceSpecCandidateInventory"); put("org.zstack.sdk.GpuDeviceSpecInventory", "org.zstack.pciDevice.gpu.GpuDeviceSpecInventory"); put("org.zstack.sdk.GpuVendor", "org.zstack.pciDevice.gpu.GpuVendor"); put("org.zstack.sdk.GuestOsCharacterInventory", "org.zstack.core.config.GuestOsCharacterInventory"); @@ -1206,6 +1235,7 @@ public class SourceClassMap { put("org.zstack.sdk.LogType", "org.zstack.log.server.LogType"); put("org.zstack.sdk.LoginAuthenticationProcedureDesc", "org.zstack.header.identity.login.LoginAuthenticationProcedureDesc"); put("org.zstack.sdk.LongJobInventory", "org.zstack.header.longjob.LongJobInventory"); + put("org.zstack.sdk.LongJobProgressDetail", "org.zstack.header.core.progress.LongJobProgressDetail"); put("org.zstack.sdk.LongJobState", "org.zstack.header.longjob.LongJobState"); put("org.zstack.sdk.LunInventory", "org.zstack.header.storageDevice.LunInventory"); put("org.zstack.sdk.MaaSUsage", "org.zstack.ai.message.MaaSUsage"); @@ -1327,6 +1357,8 @@ public class SourceClassMap { put("org.zstack.sdk.PciDeviceState", "org.zstack.pciDevice.PciDeviceState"); put("org.zstack.sdk.PciDeviceStatus", "org.zstack.pciDevice.PciDeviceStatus"); put("org.zstack.sdk.PciDeviceType", "org.zstack.pciDevice.PciDeviceType"); + put("org.zstack.sdk.PciDeviceVirtMode", "org.zstack.pciDevice.virtual.PciDeviceVirtMode"); + put("org.zstack.sdk.PciDeviceVirtState", "org.zstack.pciDevice.virtual.PciDeviceVirtState"); put("org.zstack.sdk.PciDeviceVirtStatus", "org.zstack.pciDevice.virtual.PciDeviceVirtStatus"); put("org.zstack.sdk.PendingTaskInfo", "org.zstack.header.core.progress.PendingTaskInfo"); put("org.zstack.sdk.PhysicalDriveSmartSelfTestHistoryInventory", "org.zstack.storage.device.localRaid.PhysicalDriveSmartSelfTestHistoryInventory"); @@ -1535,6 +1567,8 @@ public class SourceClassMap { put("org.zstack.sdk.VmInstancePciDeviceSpecRefInventory", "org.zstack.pciDevice.specification.pci.VmInstancePciDeviceSpecRefInventory"); put("org.zstack.sdk.VmMemoryBillingInventory", "org.zstack.billing.generator.vm.memory.VmMemoryBillingInventory"); put("org.zstack.sdk.VmMemorySpendingDetails", "org.zstack.billing.spendingcalculator.vm.VmMemorySpendingDetails"); + put("org.zstack.sdk.VmModelMountInventory", "org.zstack.ai.entity.VmModelMountInventory"); + put("org.zstack.sdk.VmModelMountStatus", "org.zstack.ai.entity.VmModelMountStatus"); put("org.zstack.sdk.VmNicBandwidthSpendingDetails", "org.zstack.billing.spendingcalculator.vmnic.VmNicBandwidthSpendingDetails"); put("org.zstack.sdk.VmNicInventory", "org.zstack.header.vm.VmNicInventory"); put("org.zstack.sdk.VmNicSecurityGroupRefInventory", "org.zstack.network.securitygroup.VmNicSecurityGroupRefInventory"); @@ -1744,6 +1778,10 @@ public class SourceClassMap { put("org.zstack.sdk.zwatch.monitorgroup.entity.MonitorGroupTemplateRefInventory", "org.zstack.zwatch.monitorgroup.entity.MonitorGroupTemplateRefInventory"); put("org.zstack.sdk.zwatch.monitorgroup.entity.MonitorGroupTemplateRefVO", "org.zstack.zwatch.monitorgroup.entity.MonitorGroupTemplateRefVO"); put("org.zstack.sdk.zwatch.monitorgroup.entity.MonitorTemplateInventory", "org.zstack.zwatch.monitorgroup.entity.MonitorTemplateInventory"); + put("org.zstack.sdk.zwatch.resnotify.ResNotifySubscriptionInventory", "org.zstack.zwatch.resnotify.ResNotifySubscriptionInventory"); + put("org.zstack.sdk.zwatch.resnotify.ResNotifySubscriptionState", "org.zstack.zwatch.resnotify.ResNotifySubscriptionState"); + put("org.zstack.sdk.zwatch.resnotify.ResNotifyType", "org.zstack.zwatch.resnotify.ResNotifyType"); + put("org.zstack.sdk.zwatch.resnotify.ResNotifyWebhookRefInventory", "org.zstack.zwatch.resnotify.ResNotifyWebhookRefInventory"); put("org.zstack.sdk.zwatch.ruleengine.ComparisonOperator", "org.zstack.zwatch.ruleengine.ComparisonOperator"); put("org.zstack.sdk.zwatch.thirdparty.entity.SNSEndpointThirdpartyAlertHistoryInventory", "org.zstack.zwatch.thirdparty.entity.SNSEndpointThirdpartyAlertHistoryInventory"); put("org.zstack.sdk.zwatch.thirdparty.entity.ThirdpartyOriginalAlertInventory", "org.zstack.zwatch.thirdparty.entity.ThirdpartyOriginalAlertInventory"); diff --git a/sdk/src/main/java/org/zstack/sdk/AccessKeyInventory.java b/sdk/src/main/java/org/zstack/sdk/AccessKeyInventory.java index 78b4cf81380..78af2d28cdd 100644 --- a/sdk/src/main/java/org/zstack/sdk/AccessKeyInventory.java +++ b/sdk/src/main/java/org/zstack/sdk/AccessKeyInventory.java @@ -1,6 +1,7 @@ package org.zstack.sdk; import org.zstack.sdk.AccessKeyState; +import org.zstack.sdk.AccessKeyType; public class AccessKeyInventory { @@ -60,6 +61,14 @@ public AccessKeyState getState() { return this.state; } + public AccessKeyType type; + public void setType(AccessKeyType type) { + this.type = type; + } + public AccessKeyType getType() { + return this.type; + } + public java.sql.Timestamp createDate; public void setCreateDate(java.sql.Timestamp createDate) { this.createDate = createDate; diff --git a/sdk/src/main/java/org/zstack/sdk/AccessKeyType.java b/sdk/src/main/java/org/zstack/sdk/AccessKeyType.java new file mode 100644 index 00000000000..9e7df4f621c --- /dev/null +++ b/sdk/src/main/java/org/zstack/sdk/AccessKeyType.java @@ -0,0 +1,6 @@ +package org.zstack.sdk; + +public enum AccessKeyType { + User, + System, +} diff --git a/sdk/src/main/java/org/zstack/sdk/AddBareMetal2DpuChassisAction.java b/sdk/src/main/java/org/zstack/sdk/AddBareMetal2DpuChassisAction.java new file mode 100644 index 00000000000..56f877caf5c --- /dev/null +++ b/sdk/src/main/java/org/zstack/sdk/AddBareMetal2DpuChassisAction.java @@ -0,0 +1,125 @@ +package org.zstack.sdk; + +import java.util.HashMap; +import java.util.Map; +import org.zstack.sdk.*; + +public class AddBareMetal2DpuChassisAction 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.AddBareMetal2ChassisResult 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, maxLength = 2048, nonempty = false, nullElements = false, emptyString = true, noTrim = false) + public java.lang.String url; + + @Param(required = true, maxLength = 255, nonempty = false, nullElements = false, emptyString = true, noTrim = false) + public java.lang.String vendorType; + + @Param(required = false, nonempty = false, nullElements = false, emptyString = true, noTrim = false) + public java.lang.String config; + + @Param(required = true, maxLength = 255, nonempty = false, nullElements = false, emptyString = false, noTrim = false) + public java.lang.String name; + + @Param(required = false, maxLength = 2048, nonempty = false, nullElements = false, emptyString = true, noTrim = false) + public java.lang.String description; + + @Param(required = true, nonempty = false, nullElements = false, emptyString = true, noTrim = false) + public java.lang.String clusterUuid; + + @Param(required = false, validValues = {"Remote","Local","Direct"}, nonempty = false, nullElements = false, emptyString = true, noTrim = false) + public java.lang.String provisionType = "Remote"; + + @Param(required = false) + public java.lang.String resourceUuid; + + @Param(required = false, nonempty = false, nullElements = false, emptyString = true, noTrim = false) + public java.util.List tagUuids; + + @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.AddBareMetal2ChassisResult value = res.getResult(org.zstack.sdk.AddBareMetal2ChassisResult.class); + ret.value = value == null ? new org.zstack.sdk.AddBareMetal2ChassisResult() : 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 = "/baremetal2/chassis/dpu"; + info.needSession = true; + info.needPoll = true; + info.parameterName = "params"; + return info; + } + +} diff --git a/sdk/src/main/java/org/zstack/sdk/AddExternalServiceConfigurationAction.java b/sdk/src/main/java/org/zstack/sdk/AddExternalServiceConfigurationAction.java new file mode 100644 index 00000000000..2a7bf9f9df1 --- /dev/null +++ b/sdk/src/main/java/org/zstack/sdk/AddExternalServiceConfigurationAction.java @@ -0,0 +1,113 @@ +package org.zstack.sdk; + +import java.util.HashMap; +import java.util.Map; +import org.zstack.sdk.*; + +public class AddExternalServiceConfigurationAction 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.AddExternalServiceConfigurationResult 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 = false, nullElements = false, emptyString = true, noTrim = false) + public java.lang.String externalServiceType; + + @Param(required = true, maxLength = 65535, nonempty = false, nullElements = false, emptyString = true, noTrim = false) + public java.lang.String configuration; + + @Param(required = false, maxLength = 2048, nonempty = false, nullElements = false, emptyString = true, noTrim = false) + public java.lang.String description; + + @Param(required = false) + public java.lang.String resourceUuid; + + @Param(required = false, nonempty = false, nullElements = false, emptyString = true, noTrim = false) + public java.util.List tagUuids; + + @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.AddExternalServiceConfigurationResult value = res.getResult(org.zstack.sdk.AddExternalServiceConfigurationResult.class); + ret.value = value == null ? new org.zstack.sdk.AddExternalServiceConfigurationResult() : 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 = "/external/service/configuration"; + info.needSession = true; + info.needPoll = true; + info.parameterName = "params"; + return info; + } + +} diff --git a/sdk/src/main/java/org/zstack/sdk/AddExternalServiceConfigurationResult.java b/sdk/src/main/java/org/zstack/sdk/AddExternalServiceConfigurationResult.java new file mode 100644 index 00000000000..743bd847d7e --- /dev/null +++ b/sdk/src/main/java/org/zstack/sdk/AddExternalServiceConfigurationResult.java @@ -0,0 +1,14 @@ +package org.zstack.sdk; + +import org.zstack.sdk.ExternalServiceConfigurationInventory; + +public class AddExternalServiceConfigurationResult { + public ExternalServiceConfigurationInventory inventory; + public void setInventory(ExternalServiceConfigurationInventory inventory) { + this.inventory = inventory; + } + public ExternalServiceConfigurationInventory getInventory() { + return this.inventory; + } + +} diff --git a/sdk/src/main/java/org/zstack/sdk/AddLogServerAction.java b/sdk/src/main/java/org/zstack/sdk/AddLogServerAction.java index 54344e22dc4..1fceee88af9 100644 --- a/sdk/src/main/java/org/zstack/sdk/AddLogServerAction.java +++ b/sdk/src/main/java/org/zstack/sdk/AddLogServerAction.java @@ -31,7 +31,7 @@ public Result throwExceptionIfError() { @Param(required = false, maxLength = 2048, nonempty = false, nullElements = false, emptyString = true, noTrim = false) public java.lang.String description; - @Param(required = true, validValues = {"ManagementNodeLog","PlatformOperationLog"}, nonempty = false, nullElements = false, emptyString = true, noTrim = false) + @Param(required = true, validValues = {"ManagementNodeLog","PlatformOperationLog","SnatLog"}, nonempty = false, nullElements = false, emptyString = true, noTrim = false) public java.lang.String category; @Param(required = true, validValues = {"Log4j2","FluentBit"}, nonempty = false, nullElements = false, emptyString = true, noTrim = false) diff --git a/sdk/src/main/java/org/zstack/sdk/BareMetal2DpuChassisConfig.java b/sdk/src/main/java/org/zstack/sdk/BareMetal2DpuChassisConfig.java new file mode 100644 index 00000000000..a4c6b03fd08 --- /dev/null +++ b/sdk/src/main/java/org/zstack/sdk/BareMetal2DpuChassisConfig.java @@ -0,0 +1,39 @@ +package org.zstack.sdk; + + + +public class BareMetal2DpuChassisConfig { + + public java.lang.String ipmiAddress; + public void setIpmiAddress(java.lang.String ipmiAddress) { + this.ipmiAddress = ipmiAddress; + } + public java.lang.String getIpmiAddress() { + return this.ipmiAddress; + } + + public int ipmiPort; + public void setIpmiPort(int ipmiPort) { + this.ipmiPort = ipmiPort; + } + public int getIpmiPort() { + return this.ipmiPort; + } + + public java.lang.String ipmiUsername; + public void setIpmiUsername(java.lang.String ipmiUsername) { + this.ipmiUsername = ipmiUsername; + } + public java.lang.String getIpmiUsername() { + return this.ipmiUsername; + } + + public java.lang.String ipmiPassword; + public void setIpmiPassword(java.lang.String ipmiPassword) { + this.ipmiPassword = ipmiPassword; + } + public java.lang.String getIpmiPassword() { + return this.ipmiPassword; + } + +} diff --git a/sdk/src/main/java/org/zstack/sdk/BareMetal2DpuChassisInventory.java b/sdk/src/main/java/org/zstack/sdk/BareMetal2DpuChassisInventory.java new file mode 100644 index 00000000000..0eaffdc2162 --- /dev/null +++ b/sdk/src/main/java/org/zstack/sdk/BareMetal2DpuChassisInventory.java @@ -0,0 +1,24 @@ +package org.zstack.sdk; + +import org.zstack.sdk.BareMetal2DpuChassisConfig; +import org.zstack.sdk.BareMetal2DpuHostInventory; + +public class BareMetal2DpuChassisInventory extends org.zstack.sdk.BareMetal2ChassisInventory { + + public BareMetal2DpuChassisConfig config; + public void setConfig(BareMetal2DpuChassisConfig config) { + this.config = config; + } + public BareMetal2DpuChassisConfig getConfig() { + return this.config; + } + + public BareMetal2DpuHostInventory dpuHost; + public void setDpuHost(BareMetal2DpuHostInventory dpuHost) { + this.dpuHost = dpuHost; + } + public BareMetal2DpuHostInventory getDpuHost() { + return this.dpuHost; + } + +} diff --git a/sdk/src/main/java/org/zstack/sdk/BareMetal2DpuHostInventory.java b/sdk/src/main/java/org/zstack/sdk/BareMetal2DpuHostInventory.java new file mode 100644 index 00000000000..09864707a7a --- /dev/null +++ b/sdk/src/main/java/org/zstack/sdk/BareMetal2DpuHostInventory.java @@ -0,0 +1,31 @@ +package org.zstack.sdk; + + + +public class BareMetal2DpuHostInventory extends org.zstack.sdk.HostInventory { + + public java.lang.String url; + public void setUrl(java.lang.String url) { + this.url = url; + } + public java.lang.String getUrl() { + return this.url; + } + + public java.lang.String vendorType; + public void setVendorType(java.lang.String vendorType) { + this.vendorType = vendorType; + } + public java.lang.String getVendorType() { + return this.vendorType; + } + + public java.lang.String chassisUuid; + public void setChassisUuid(java.lang.String chassisUuid) { + this.chassisUuid = chassisUuid; + } + public java.lang.String getChassisUuid() { + return this.chassisUuid; + } + +} diff --git a/sdk/src/main/java/org/zstack/sdk/ChangeVmNicNetworkAction.java b/sdk/src/main/java/org/zstack/sdk/ChangeVmNicNetworkAction.java index ffec933f2cc..59beff6880a 100644 --- a/sdk/src/main/java/org/zstack/sdk/ChangeVmNicNetworkAction.java +++ b/sdk/src/main/java/org/zstack/sdk/ChangeVmNicNetworkAction.java @@ -34,6 +34,9 @@ public Result throwExceptionIfError() { @Param(required = false) public java.lang.String staticIp; + @Param(required = false, nonempty = false, nullElements = false, emptyString = true, noTrim = false) + public java.util.List dnsAddresses; + @Param(required = false) public java.util.List systemTags; diff --git a/sdk/src/main/java/org/zstack/sdk/CreateAccessKeyAction.java b/sdk/src/main/java/org/zstack/sdk/CreateAccessKeyAction.java index b76d75eee41..e69e06cc241 100644 --- a/sdk/src/main/java/org/zstack/sdk/CreateAccessKeyAction.java +++ b/sdk/src/main/java/org/zstack/sdk/CreateAccessKeyAction.java @@ -40,6 +40,9 @@ public Result throwExceptionIfError() { @Param(required = false, maxLength = 40, minLength = 10, nonempty = false, nullElements = false, emptyString = true, noTrim = false) public java.lang.String AccessKeySecret; + @Param(required = false, validValues = {"User","System"}, nonempty = false, nullElements = false, emptyString = true, noTrim = false) + public java.lang.String accessKeyType = "User"; + @Param(required = false) public java.lang.String resourceUuid; diff --git a/sdk/src/main/java/org/zstack/sdk/CreateBareMetal2InstanceAction.java b/sdk/src/main/java/org/zstack/sdk/CreateBareMetal2InstanceAction.java index 755fb104476..a0fac8d2d9f 100644 --- a/sdk/src/main/java/org/zstack/sdk/CreateBareMetal2InstanceAction.java +++ b/sdk/src/main/java/org/zstack/sdk/CreateBareMetal2InstanceAction.java @@ -70,6 +70,9 @@ public Result throwExceptionIfError() { @Param(required = false, nonempty = false, nullElements = false, emptyString = true, noTrim = false) public java.lang.String gatewayAllocatorStrategy; + @Param(required = false, validValues = {"IPMI","DPU"}, nonempty = false, nullElements = false, emptyString = true, noTrim = false) + public java.lang.String chassisType; + @Param(required = false) public java.lang.String resourceUuid; diff --git a/sdk/src/main/java/org/zstack/sdk/CreateClusterAction.java b/sdk/src/main/java/org/zstack/sdk/CreateClusterAction.java index 4d489de2ea8..5975cf8f9fd 100644 --- a/sdk/src/main/java/org/zstack/sdk/CreateClusterAction.java +++ b/sdk/src/main/java/org/zstack/sdk/CreateClusterAction.java @@ -34,7 +34,7 @@ public Result throwExceptionIfError() { @Param(required = false, maxLength = 2048, nonempty = false, nullElements = false, emptyString = true, noTrim = false) public java.lang.String description; - @Param(required = true, validValues = {"KVM","Simulator","baremetal","baremetal2","xdragon"}, nonempty = false, nullElements = false, emptyString = true, noTrim = false) + @Param(required = true, validValues = {"KVM","Simulator","baremetal","baremetal2","xdragon","baremetal2Dpu"}, nonempty = false, nullElements = false, emptyString = true, noTrim = false) public java.lang.String hypervisorType; @Param(required = false, validValues = {"zstack","baremetal","baremetal2"}, nonempty = false, nullElements = false, emptyString = true, noTrim = false) diff --git a/sdk/src/main/java/org/zstack/sdk/DGpuDeviceInventory.java b/sdk/src/main/java/org/zstack/sdk/DGpuDeviceInventory.java new file mode 100644 index 00000000000..c8fb16ef2cf --- /dev/null +++ b/sdk/src/main/java/org/zstack/sdk/DGpuDeviceInventory.java @@ -0,0 +1,127 @@ +package org.zstack.sdk; + +import org.zstack.sdk.DGpuStatus; + +public class DGpuDeviceInventory { + + public java.lang.String uuid; + public void setUuid(java.lang.String uuid) { + this.uuid = uuid; + } + public java.lang.String getUuid() { + return this.uuid; + } + + public java.lang.String name; + public void setName(java.lang.String name) { + this.name = name; + } + public java.lang.String getName() { + return this.name; + } + + public java.lang.String parentGpuUuid; + public void setParentGpuUuid(java.lang.String parentGpuUuid) { + this.parentGpuUuid = parentGpuUuid; + } + public java.lang.String getParentGpuUuid() { + return this.parentGpuUuid; + } + + public java.lang.String gpuSpecUuid; + public void setGpuSpecUuid(java.lang.String gpuSpecUuid) { + this.gpuSpecUuid = gpuSpecUuid; + } + public java.lang.String getGpuSpecUuid() { + return this.gpuSpecUuid; + } + + public java.lang.String hostUuid; + public void setHostUuid(java.lang.String hostUuid) { + this.hostUuid = hostUuid; + } + public java.lang.String getHostUuid() { + return this.hostUuid; + } + + public java.lang.String vmInstanceUuid; + public void setVmInstanceUuid(java.lang.String vmInstanceUuid) { + this.vmInstanceUuid = vmInstanceUuid; + } + public java.lang.String getVmInstanceUuid() { + return this.vmInstanceUuid; + } + + public java.lang.Long allocatedMemory; + public void setAllocatedMemory(java.lang.Long allocatedMemory) { + this.allocatedMemory = allocatedMemory; + } + public java.lang.Long getAllocatedMemory() { + return this.allocatedMemory; + } + + public java.lang.Long shmemSize; + public void setShmemSize(java.lang.Long shmemSize) { + this.shmemSize = shmemSize; + } + public java.lang.Long getShmemSize() { + return this.shmemSize; + } + + public java.lang.String protocol; + public void setProtocol(java.lang.String protocol) { + this.protocol = protocol; + } + public java.lang.String getProtocol() { + return this.protocol; + } + + public java.lang.Integer smPercentLimit; + public void setSmPercentLimit(java.lang.Integer smPercentLimit) { + this.smPercentLimit = smPercentLimit; + } + public java.lang.Integer getSmPercentLimit() { + return this.smPercentLimit; + } + + public DGpuStatus status; + public void setStatus(DGpuStatus status) { + this.status = status; + } + public DGpuStatus getStatus() { + return this.status; + } + + public java.sql.Timestamp createDate; + public void setCreateDate(java.sql.Timestamp createDate) { + this.createDate = createDate; + } + public java.sql.Timestamp getCreateDate() { + return this.createDate; + } + + public java.sql.Timestamp lastOpDate; + public void setLastOpDate(java.sql.Timestamp lastOpDate) { + this.lastOpDate = lastOpDate; + } + public java.sql.Timestamp getLastOpDate() { + return this.lastOpDate; + } + + public java.lang.String vendorId; + public void setVendorId(java.lang.String vendorId) { + this.vendorId = vendorId; + } + public java.lang.String getVendorId() { + return this.vendorId; + } + + public java.lang.String vendor; + public void setVendor(java.lang.String vendor) { + this.vendor = vendor; + } + public java.lang.String getVendor() { + return this.vendor; + } + +} diff --git a/sdk/src/main/java/org/zstack/sdk/DGpuProfileInventory.java b/sdk/src/main/java/org/zstack/sdk/DGpuProfileInventory.java new file mode 100644 index 00000000000..96a5568810c --- /dev/null +++ b/sdk/src/main/java/org/zstack/sdk/DGpuProfileInventory.java @@ -0,0 +1,55 @@ +package org.zstack.sdk; + + + +public class DGpuProfileInventory { + + public java.lang.String uuid; + public void setUuid(java.lang.String uuid) { + this.uuid = uuid; + } + public java.lang.String getUuid() { + return this.uuid; + } + + public java.lang.String gpuSpecUuid; + public void setGpuSpecUuid(java.lang.String gpuSpecUuid) { + this.gpuSpecUuid = gpuSpecUuid; + } + public java.lang.String getGpuSpecUuid() { + return this.gpuSpecUuid; + } + + public java.lang.Long memorySize; + public void setMemorySize(java.lang.Long memorySize) { + this.memorySize = memorySize; + } + public java.lang.Long getMemorySize() { + return this.memorySize; + } + + public java.lang.Long shmemSize; + public void setShmemSize(java.lang.Long shmemSize) { + this.shmemSize = shmemSize; + } + public java.lang.Long getShmemSize() { + return this.shmemSize; + } + + public java.sql.Timestamp createDate; + public void setCreateDate(java.sql.Timestamp createDate) { + this.createDate = createDate; + } + public java.sql.Timestamp getCreateDate() { + return this.createDate; + } + + public java.sql.Timestamp lastOpDate; + public void setLastOpDate(java.sql.Timestamp lastOpDate) { + this.lastOpDate = lastOpDate; + } + public java.sql.Timestamp getLastOpDate() { + return this.lastOpDate; + } + +} diff --git a/sdk/src/main/java/org/zstack/sdk/DGpuSpecStatsInventory.java b/sdk/src/main/java/org/zstack/sdk/DGpuSpecStatsInventory.java new file mode 100644 index 00000000000..2191e5c49c3 --- /dev/null +++ b/sdk/src/main/java/org/zstack/sdk/DGpuSpecStatsInventory.java @@ -0,0 +1,79 @@ +package org.zstack.sdk; + + + +public class DGpuSpecStatsInventory { + + public java.lang.String gpuSpecUuid; + public void setGpuSpecUuid(java.lang.String gpuSpecUuid) { + this.gpuSpecUuid = gpuSpecUuid; + } + public java.lang.String getGpuSpecUuid() { + return this.gpuSpecUuid; + } + + public java.lang.String gpuSpecName; + public void setGpuSpecName(java.lang.String gpuSpecName) { + this.gpuSpecName = gpuSpecName; + } + public java.lang.String getGpuSpecName() { + return this.gpuSpecName; + } + + public java.lang.String gpuType; + public void setGpuType(java.lang.String gpuType) { + this.gpuType = gpuType; + } + public java.lang.String getGpuType() { + return this.gpuType; + } + + public java.lang.Long gpuCount; + public void setGpuCount(java.lang.Long gpuCount) { + this.gpuCount = gpuCount; + } + public java.lang.Long getGpuCount() { + return this.gpuCount; + } + + public java.lang.Long dgpuCount; + public void setDgpuCount(java.lang.Long dgpuCount) { + this.dgpuCount = dgpuCount; + } + public java.lang.Long getDgpuCount() { + return this.dgpuCount; + } + + public java.lang.Long totalMemory; + public void setTotalMemory(java.lang.Long totalMemory) { + this.totalMemory = totalMemory; + } + public java.lang.Long getTotalMemory() { + return this.totalMemory; + } + + public java.lang.Long allocatedMemory; + public void setAllocatedMemory(java.lang.Long allocatedMemory) { + this.allocatedMemory = allocatedMemory; + } + public java.lang.Long getAllocatedMemory() { + return this.allocatedMemory; + } + + public java.lang.Long availableMemory; + public void setAvailableMemory(java.lang.Long availableMemory) { + this.availableMemory = availableMemory; + } + public java.lang.Long getAvailableMemory() { + return this.availableMemory; + } + + public java.lang.Double allocationRate; + public void setAllocationRate(java.lang.Double allocationRate) { + this.allocationRate = allocationRate; + } + public java.lang.Double getAllocationRate() { + return this.allocationRate; + } + +} diff --git a/sdk/src/main/java/org/zstack/sdk/DGpuStatus.java b/sdk/src/main/java/org/zstack/sdk/DGpuStatus.java new file mode 100644 index 00000000000..f2c72fc7ba7 --- /dev/null +++ b/sdk/src/main/java/org/zstack/sdk/DGpuStatus.java @@ -0,0 +1,8 @@ +package org.zstack.sdk; + +public enum DGpuStatus { + Normal, + Fault, + Unknown, + Disconnected, +} diff --git a/sdk/src/main/java/org/zstack/sdk/DeleteExternalServiceConfigurationAction.java b/sdk/src/main/java/org/zstack/sdk/DeleteExternalServiceConfigurationAction.java new file mode 100644 index 00000000000..3f66ea0b137 --- /dev/null +++ b/sdk/src/main/java/org/zstack/sdk/DeleteExternalServiceConfigurationAction.java @@ -0,0 +1,104 @@ +package org.zstack.sdk; + +import java.util.HashMap; +import java.util.Map; +import org.zstack.sdk.*; + +public class DeleteExternalServiceConfigurationAction 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.DeleteExternalServiceConfigurationResult 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 = false, nullElements = false, emptyString = true, noTrim = false) + public java.lang.String uuid; + + @Param(required = false) + public java.lang.String deleteMode = "Permissive"; + + @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.DeleteExternalServiceConfigurationResult value = res.getResult(org.zstack.sdk.DeleteExternalServiceConfigurationResult.class); + ret.value = value == null ? new org.zstack.sdk.DeleteExternalServiceConfigurationResult() : 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 = "DELETE"; + info.path = "/external/service/configuration/{uuid}"; + info.needSession = true; + info.needPoll = true; + info.parameterName = ""; + return info; + } + +} diff --git a/sdk/src/main/java/org/zstack/sdk/DeleteExternalServiceConfigurationResult.java b/sdk/src/main/java/org/zstack/sdk/DeleteExternalServiceConfigurationResult.java new file mode 100644 index 00000000000..22f9163d915 --- /dev/null +++ b/sdk/src/main/java/org/zstack/sdk/DeleteExternalServiceConfigurationResult.java @@ -0,0 +1,7 @@ +package org.zstack.sdk; + + + +public class DeleteExternalServiceConfigurationResult { + +} diff --git a/sdk/src/main/java/org/zstack/sdk/DeployAppDevelopmentServiceAction.java b/sdk/src/main/java/org/zstack/sdk/DeployAppDevelopmentServiceAction.java index 0d4ef73088b..4fdecd4a475 100644 --- a/sdk/src/main/java/org/zstack/sdk/DeployAppDevelopmentServiceAction.java +++ b/sdk/src/main/java/org/zstack/sdk/DeployAppDevelopmentServiceAction.java @@ -55,6 +55,9 @@ public Result throwExceptionIfError() { @Param(required = false, nonempty = false, nullElements = false, emptyString = true, noTrim = false) public java.lang.Integer cpuNum; + @Param(required = false, nonempty = false, nullElements = false, emptyString = true, noTrim = false) + public java.lang.Integer requestCpuNum; + @Param(required = true, nonempty = false, nullElements = false, emptyString = true, noTrim = false) public java.lang.String name; @@ -73,6 +76,9 @@ public Result throwExceptionIfError() { @Param(required = false, nonempty = false, nullElements = false, emptyString = true, noTrim = false) public java.lang.Long memorySize; + @Param(required = false, nonempty = false, nullElements = false, emptyString = true, noTrim = false) + public java.lang.Long requestMemorySize; + @Param(required = false, nonempty = false, nullElements = false, emptyString = true, noTrim = false) public java.util.List l3NetworkUuids; diff --git a/sdk/src/main/java/org/zstack/sdk/DeployModelEvalServiceAction.java b/sdk/src/main/java/org/zstack/sdk/DeployModelEvalServiceAction.java index f29c06a748e..a75b1f851ec 100644 --- a/sdk/src/main/java/org/zstack/sdk/DeployModelEvalServiceAction.java +++ b/sdk/src/main/java/org/zstack/sdk/DeployModelEvalServiceAction.java @@ -106,6 +106,9 @@ public Result throwExceptionIfError() { @Param(required = false, nonempty = false, nullElements = false, emptyString = true, noTrim = false) public java.lang.Integer cpuNum; + @Param(required = false, nonempty = false, nullElements = false, emptyString = true, noTrim = false) + public java.lang.Integer requestCpuNum; + @Param(required = true, nonempty = false, nullElements = false, emptyString = true, noTrim = false) public java.lang.String name; @@ -124,6 +127,9 @@ public Result throwExceptionIfError() { @Param(required = false, nonempty = false, nullElements = false, emptyString = true, noTrim = false) public java.lang.Long memorySize; + @Param(required = false, nonempty = false, nullElements = false, emptyString = true, noTrim = false) + public java.lang.Long requestMemorySize; + @Param(required = false, nonempty = false, nullElements = false, emptyString = true, noTrim = false) public java.util.List l3NetworkUuids; diff --git a/sdk/src/main/java/org/zstack/sdk/DeployModelServiceAction.java b/sdk/src/main/java/org/zstack/sdk/DeployModelServiceAction.java index 105e7083ae2..cbfe5811d6a 100644 --- a/sdk/src/main/java/org/zstack/sdk/DeployModelServiceAction.java +++ b/sdk/src/main/java/org/zstack/sdk/DeployModelServiceAction.java @@ -55,6 +55,9 @@ public Result throwExceptionIfError() { @Param(required = false, nonempty = false, nullElements = false, emptyString = true, noTrim = false) public java.lang.Integer cpuNum; + @Param(required = false, nonempty = false, nullElements = false, emptyString = true, noTrim = false) + public java.lang.Integer requestCpuNum; + @Param(required = true, nonempty = false, nullElements = false, emptyString = true, noTrim = false) public java.lang.String name; @@ -73,6 +76,9 @@ public Result throwExceptionIfError() { @Param(required = false, nonempty = false, nullElements = false, emptyString = true, noTrim = false) public java.lang.Long memorySize; + @Param(required = false, nonempty = false, nullElements = false, emptyString = true, noTrim = false) + public java.lang.Long requestMemorySize; + @Param(required = false, nonempty = false, nullElements = false, emptyString = true, noTrim = false) public java.util.List l3NetworkUuids; diff --git a/sdk/src/main/java/org/zstack/sdk/DetachDGpuFromVmAction.java b/sdk/src/main/java/org/zstack/sdk/DetachDGpuFromVmAction.java new file mode 100644 index 00000000000..23995825b1b --- /dev/null +++ b/sdk/src/main/java/org/zstack/sdk/DetachDGpuFromVmAction.java @@ -0,0 +1,104 @@ +package org.zstack.sdk; + +import java.util.HashMap; +import java.util.Map; +import org.zstack.sdk.*; + +public class DetachDGpuFromVmAction 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.DetachDGpuFromVmResult 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 = false, nullElements = false, emptyString = true, noTrim = false) + public java.lang.String vmInstanceUuid; + + @Param(required = true, nonempty = false, nullElements = false, emptyString = true, noTrim = false) + public java.lang.String dgpuDeviceUuid; + + @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.DetachDGpuFromVmResult value = res.getResult(org.zstack.sdk.DetachDGpuFromVmResult.class); + ret.value = value == null ? new org.zstack.sdk.DetachDGpuFromVmResult() : 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 = "PUT"; + info.path = "/vm-instances/{vmInstanceUuid}/actions"; + info.needSession = true; + info.needPoll = true; + info.parameterName = "detachDGpuFromVm"; + return info; + } + +} diff --git a/sdk/src/main/java/org/zstack/sdk/DetachDGpuFromVmResult.java b/sdk/src/main/java/org/zstack/sdk/DetachDGpuFromVmResult.java new file mode 100644 index 00000000000..3574a0e85f9 --- /dev/null +++ b/sdk/src/main/java/org/zstack/sdk/DetachDGpuFromVmResult.java @@ -0,0 +1,7 @@ +package org.zstack.sdk; + + + +public class DetachDGpuFromVmResult { + +} diff --git a/sdk/src/main/java/org/zstack/sdk/DisableDGpuModeAction.java b/sdk/src/main/java/org/zstack/sdk/DisableDGpuModeAction.java new file mode 100644 index 00000000000..8711977ee4d --- /dev/null +++ b/sdk/src/main/java/org/zstack/sdk/DisableDGpuModeAction.java @@ -0,0 +1,101 @@ +package org.zstack.sdk; + +import java.util.HashMap; +import java.util.Map; +import org.zstack.sdk.*; + +public class DisableDGpuModeAction 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.DisableDGpuModeResult 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 = false, nullElements = false, emptyString = true, noTrim = false) + public java.lang.String gpuDeviceUuid; + + @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.DisableDGpuModeResult value = res.getResult(org.zstack.sdk.DisableDGpuModeResult.class); + ret.value = value == null ? new org.zstack.sdk.DisableDGpuModeResult() : 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 = "PUT"; + info.path = "/gpu-device/gpu-devices/{gpuDeviceUuid}/actions"; + info.needSession = true; + info.needPoll = true; + info.parameterName = "disableDGpuMode"; + return info; + } + +} diff --git a/sdk/src/main/java/org/zstack/sdk/DisableDGpuModeResult.java b/sdk/src/main/java/org/zstack/sdk/DisableDGpuModeResult.java new file mode 100644 index 00000000000..51e129cf234 --- /dev/null +++ b/sdk/src/main/java/org/zstack/sdk/DisableDGpuModeResult.java @@ -0,0 +1,7 @@ +package org.zstack.sdk; + + + +public class DisableDGpuModeResult { + +} diff --git a/sdk/src/main/java/org/zstack/sdk/EnableDGpuModeAction.java b/sdk/src/main/java/org/zstack/sdk/EnableDGpuModeAction.java new file mode 100644 index 00000000000..a73ac6a3cd1 --- /dev/null +++ b/sdk/src/main/java/org/zstack/sdk/EnableDGpuModeAction.java @@ -0,0 +1,101 @@ +package org.zstack.sdk; + +import java.util.HashMap; +import java.util.Map; +import org.zstack.sdk.*; + +public class EnableDGpuModeAction 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.EnableDGpuModeResult 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 = false, nullElements = false, emptyString = true, noTrim = false) + public java.lang.String gpuDeviceUuid; + + @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.EnableDGpuModeResult value = res.getResult(org.zstack.sdk.EnableDGpuModeResult.class); + ret.value = value == null ? new org.zstack.sdk.EnableDGpuModeResult() : 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 = "PUT"; + info.path = "/gpu-device/gpu-devices/{gpuDeviceUuid}/actions"; + info.needSession = true; + info.needPoll = true; + info.parameterName = "enableDGpuMode"; + return info; + } + +} diff --git a/sdk/src/main/java/org/zstack/sdk/EnableDGpuModeResult.java b/sdk/src/main/java/org/zstack/sdk/EnableDGpuModeResult.java new file mode 100644 index 00000000000..40eb1c5f2d3 --- /dev/null +++ b/sdk/src/main/java/org/zstack/sdk/EnableDGpuModeResult.java @@ -0,0 +1,7 @@ +package org.zstack.sdk; + + + +public class EnableDGpuModeResult { + +} diff --git a/sdk/src/main/java/org/zstack/sdk/ExternalServiceConfigurationInventory.java b/sdk/src/main/java/org/zstack/sdk/ExternalServiceConfigurationInventory.java new file mode 100644 index 00000000000..05d6ca9e276 --- /dev/null +++ b/sdk/src/main/java/org/zstack/sdk/ExternalServiceConfigurationInventory.java @@ -0,0 +1,55 @@ +package org.zstack.sdk; + + + +public class ExternalServiceConfigurationInventory { + + public java.lang.String uuid; + public void setUuid(java.lang.String uuid) { + this.uuid = uuid; + } + public java.lang.String getUuid() { + return this.uuid; + } + + public java.lang.String serviceType; + public void setServiceType(java.lang.String serviceType) { + this.serviceType = serviceType; + } + public java.lang.String getServiceType() { + return this.serviceType; + } + + public java.lang.String configuration; + public void setConfiguration(java.lang.String configuration) { + this.configuration = configuration; + } + public java.lang.String getConfiguration() { + return this.configuration; + } + + public java.lang.String description; + public void setDescription(java.lang.String description) { + this.description = description; + } + public java.lang.String getDescription() { + return this.description; + } + + public java.sql.Timestamp createDate; + public void setCreateDate(java.sql.Timestamp createDate) { + this.createDate = createDate; + } + public java.sql.Timestamp getCreateDate() { + return this.createDate; + } + + public java.sql.Timestamp lastOpDate; + public void setLastOpDate(java.sql.Timestamp lastOpDate) { + this.lastOpDate = lastOpDate; + } + public java.sql.Timestamp getLastOpDate() { + return this.lastOpDate; + } + +} diff --git a/sdk/src/main/java/org/zstack/sdk/ExternalServiceInventory.java b/sdk/src/main/java/org/zstack/sdk/ExternalServiceInventory.java index 8882e2f1f49..7f57fa5da36 100644 --- a/sdk/src/main/java/org/zstack/sdk/ExternalServiceInventory.java +++ b/sdk/src/main/java/org/zstack/sdk/ExternalServiceInventory.java @@ -28,4 +28,12 @@ public ExternalServiceCapabilities getCapabilities() { return this.capabilities; } + public java.lang.String serviceType; + public void setServiceType(java.lang.String serviceType) { + this.serviceType = serviceType; + } + public java.lang.String getServiceType() { + return this.serviceType; + } + } diff --git a/sdk/src/main/java/org/zstack/sdk/GetDGpuSpecStatsAction.java b/sdk/src/main/java/org/zstack/sdk/GetDGpuSpecStatsAction.java new file mode 100644 index 00000000000..d80fe253c6b --- /dev/null +++ b/sdk/src/main/java/org/zstack/sdk/GetDGpuSpecStatsAction.java @@ -0,0 +1,101 @@ +package org.zstack.sdk; + +import java.util.HashMap; +import java.util.Map; +import org.zstack.sdk.*; + +public class GetDGpuSpecStatsAction 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.GetDGpuSpecStatsResult 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 = false, nonempty = false, nullElements = false, emptyString = true, noTrim = false) + public java.lang.String zoneUuid; + + @Param(required = false, nonempty = false, nullElements = false, emptyString = true, noTrim = false) + public java.lang.String clusterUuid; + + @Param(required = false, nonempty = false, nullElements = false, emptyString = true, noTrim = false) + public java.lang.String hostUuid; + + @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; + + + private Result makeResult(ApiResult res) { + Result ret = new Result(); + if (res.error != null) { + ret.error = res.error; + return ret; + } + + org.zstack.sdk.GetDGpuSpecStatsResult value = res.getResult(org.zstack.sdk.GetDGpuSpecStatsResult.class); + ret.value = value == null ? new org.zstack.sdk.GetDGpuSpecStatsResult() : 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 = "GET"; + info.path = "/gpu-device/dgpu-spec-stats"; + info.needSession = true; + info.needPoll = false; + info.parameterName = ""; + return info; + } + +} diff --git a/sdk/src/main/java/org/zstack/sdk/GetDGpuSpecStatsResult.java b/sdk/src/main/java/org/zstack/sdk/GetDGpuSpecStatsResult.java new file mode 100644 index 00000000000..b15d1af436f --- /dev/null +++ b/sdk/src/main/java/org/zstack/sdk/GetDGpuSpecStatsResult.java @@ -0,0 +1,14 @@ +package org.zstack.sdk; + + + +public class GetDGpuSpecStatsResult { + public java.util.List inventories; + public void setInventories(java.util.List inventories) { + this.inventories = inventories; + } + public java.util.List getInventories() { + return this.inventories; + } + +} diff --git a/sdk/src/main/java/org/zstack/sdk/GetGpuDeviceCandidatesAction.java b/sdk/src/main/java/org/zstack/sdk/GetGpuDeviceCandidatesAction.java new file mode 100644 index 00000000000..d99a6378869 --- /dev/null +++ b/sdk/src/main/java/org/zstack/sdk/GetGpuDeviceCandidatesAction.java @@ -0,0 +1,110 @@ +package org.zstack.sdk; + +import java.util.HashMap; +import java.util.Map; +import org.zstack.sdk.*; + +public class GetGpuDeviceCandidatesAction 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.GetGpuDeviceCandidatesResult 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 = false, nonempty = false, nullElements = false, emptyString = true, noTrim = false) + public java.util.List clusterUuids; + + @Param(required = false, nonempty = false, nullElements = false, emptyString = true, noTrim = false) + public java.lang.String hostUuid; + + @Param(required = false, nonempty = false, nullElements = false, emptyString = true, noTrim = false) + public java.lang.String vmInstanceUuid; + + @Param(required = false, nonempty = false, nullElements = false, emptyString = true, noTrim = false) + public java.util.List vmInstanceUuids; + + @Param(required = false, validValues = {"PCI","DGPU"}, nonempty = false, nullElements = false, emptyString = true, noTrim = false) + public java.lang.String mode; + + @Param(required = false, nonempty = false, nullElements = false, emptyString = true, noTrim = false) + public java.util.List pciSpecUuids; + + @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; + + + private Result makeResult(ApiResult res) { + Result ret = new Result(); + if (res.error != null) { + ret.error = res.error; + return ret; + } + + org.zstack.sdk.GetGpuDeviceCandidatesResult value = res.getResult(org.zstack.sdk.GetGpuDeviceCandidatesResult.class); + ret.value = value == null ? new org.zstack.sdk.GetGpuDeviceCandidatesResult() : 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 = "GET"; + info.path = "/gpu-devices/candidates"; + info.needSession = true; + info.needPoll = false; + info.parameterName = ""; + return info; + } + +} diff --git a/sdk/src/main/java/org/zstack/sdk/GetGpuDeviceCandidatesResult.java b/sdk/src/main/java/org/zstack/sdk/GetGpuDeviceCandidatesResult.java new file mode 100644 index 00000000000..f0f06878a66 --- /dev/null +++ b/sdk/src/main/java/org/zstack/sdk/GetGpuDeviceCandidatesResult.java @@ -0,0 +1,14 @@ +package org.zstack.sdk; + + + +public class GetGpuDeviceCandidatesResult { + public java.util.List inventories; + public void setInventories(java.util.List inventories) { + this.inventories = inventories; + } + public java.util.List getInventories() { + return this.inventories; + } + +} diff --git a/sdk/src/main/java/org/zstack/sdk/GetGpuDeviceSpecCandidatesAction.java b/sdk/src/main/java/org/zstack/sdk/GetGpuDeviceSpecCandidatesAction.java index 3b32815e973..c8d3de5aadc 100644 --- a/sdk/src/main/java/org/zstack/sdk/GetGpuDeviceSpecCandidatesAction.java +++ b/sdk/src/main/java/org/zstack/sdk/GetGpuDeviceSpecCandidatesAction.java @@ -37,6 +37,9 @@ public Result throwExceptionIfError() { @Param(required = false, nonempty = false, nullElements = false, emptyString = true, noTrim = false) public java.util.List vmInstanceUuids; + @Param(required = false, validValues = {"PCI","DGPU"}, nonempty = false, nullElements = false, emptyString = true, noTrim = false) + public java.lang.String mode; + @Param(required = false) public java.util.List systemTags; diff --git a/sdk/src/main/java/org/zstack/sdk/GetVmSchedulingRulesExecuteStateAction.java b/sdk/src/main/java/org/zstack/sdk/GetVmSchedulingRulesExecuteStateAction.java index 6a15d7c436e..78650d6e7f9 100644 --- a/sdk/src/main/java/org/zstack/sdk/GetVmSchedulingRulesExecuteStateAction.java +++ b/sdk/src/main/java/org/zstack/sdk/GetVmSchedulingRulesExecuteStateAction.java @@ -84,11 +84,11 @@ protected Map getNonAPIParameterMap() { protected RestInfo getRestInfo() { RestInfo info = new RestInfo(); - info.httpMethod = "POST"; + info.httpMethod = "GET"; info.path = "/get/vmSchedulingRules/conflict/state"; info.needSession = true; info.needPoll = false; - info.parameterName = "params"; + info.parameterName = ""; return info; } diff --git a/sdk/src/main/java/org/zstack/sdk/GetVmsSchedulingStateFromSchedulingRuleAction.java b/sdk/src/main/java/org/zstack/sdk/GetVmsSchedulingStateFromSchedulingRuleAction.java index 83ae01b33e5..1c9c6a69ab0 100644 --- a/sdk/src/main/java/org/zstack/sdk/GetVmsSchedulingStateFromSchedulingRuleAction.java +++ b/sdk/src/main/java/org/zstack/sdk/GetVmsSchedulingStateFromSchedulingRuleAction.java @@ -87,11 +87,11 @@ protected Map getNonAPIParameterMap() { protected RestInfo getRestInfo() { RestInfo info = new RestInfo(); - info.httpMethod = "POST"; + info.httpMethod = "GET"; info.path = "/get/vms/schedulingState/from/SchedulingRule"; info.needSession = true; info.needPoll = false; - info.parameterName = "params"; + info.parameterName = ""; return info; } diff --git a/sdk/src/main/java/org/zstack/sdk/GetVpcVRouterSnatLogStateAction.java b/sdk/src/main/java/org/zstack/sdk/GetVpcVRouterSnatLogStateAction.java new file mode 100644 index 00000000000..bccc21c32e3 --- /dev/null +++ b/sdk/src/main/java/org/zstack/sdk/GetVpcVRouterSnatLogStateAction.java @@ -0,0 +1,95 @@ +package org.zstack.sdk; + +import java.util.HashMap; +import java.util.Map; +import org.zstack.sdk.*; + +public class GetVpcVRouterSnatLogStateAction 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.GetVpcVRouterSnatLogStateResult 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 = false, nullElements = false, emptyString = true, noTrim = false) + public java.lang.String uuid; + + @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; + + + private Result makeResult(ApiResult res) { + Result ret = new Result(); + if (res.error != null) { + ret.error = res.error; + return ret; + } + + org.zstack.sdk.GetVpcVRouterSnatLogStateResult value = res.getResult(org.zstack.sdk.GetVpcVRouterSnatLogStateResult.class); + ret.value = value == null ? new org.zstack.sdk.GetVpcVRouterSnatLogStateResult() : 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 = "GET"; + info.path = "/vpc/virtual-routers/{uuid}/snat-log"; + info.needSession = true; + info.needPoll = false; + info.parameterName = ""; + return info; + } + +} diff --git a/sdk/src/main/java/org/zstack/sdk/GetVpcVRouterSnatLogStateResult.java b/sdk/src/main/java/org/zstack/sdk/GetVpcVRouterSnatLogStateResult.java new file mode 100644 index 00000000000..cee48dd497c --- /dev/null +++ b/sdk/src/main/java/org/zstack/sdk/GetVpcVRouterSnatLogStateResult.java @@ -0,0 +1,14 @@ +package org.zstack.sdk; + + + +public class GetVpcVRouterSnatLogStateResult { + public java.lang.String state; + public void setState(java.lang.String state) { + this.state = state; + } + public java.lang.String getState() { + return this.state; + } + +} diff --git a/sdk/src/main/java/org/zstack/sdk/GpuDeviceInventory.java b/sdk/src/main/java/org/zstack/sdk/GpuDeviceInventory.java index 587c9cbeae3..0a47629a7b3 100644 --- a/sdk/src/main/java/org/zstack/sdk/GpuDeviceInventory.java +++ b/sdk/src/main/java/org/zstack/sdk/GpuDeviceInventory.java @@ -60,4 +60,12 @@ public GpuAllocateStatus getAllocateStatus() { return this.allocateStatus; } + public java.lang.String mode; + public void setMode(java.lang.String mode) { + this.mode = mode; + } + public java.lang.String getMode() { + return this.mode; + } + } diff --git a/sdk/src/main/java/org/zstack/sdk/GpuDeviceSpecCandidateInventory.java b/sdk/src/main/java/org/zstack/sdk/GpuDeviceSpecCandidateInventory.java new file mode 100644 index 00000000000..60eb7da3345 --- /dev/null +++ b/sdk/src/main/java/org/zstack/sdk/GpuDeviceSpecCandidateInventory.java @@ -0,0 +1,31 @@ +package org.zstack.sdk; + + + +public class GpuDeviceSpecCandidateInventory extends org.zstack.sdk.GpuDeviceSpecInventory { + + public java.lang.String mode; + public void setMode(java.lang.String mode) { + this.mode = mode; + } + public java.lang.String getMode() { + return this.mode; + } + + public java.lang.Long maxAvailableMemory; + public void setMaxAvailableMemory(java.lang.Long maxAvailableMemory) { + this.maxAvailableMemory = maxAvailableMemory; + } + public java.lang.Long getMaxAvailableMemory() { + return this.maxAvailableMemory; + } + + public java.util.List dgpuProfiles; + public void setDgpuProfiles(java.util.List dgpuProfiles) { + this.dgpuProfiles = dgpuProfiles; + } + public java.util.List getDgpuProfiles() { + return this.dgpuProfiles; + } + +} diff --git a/sdk/src/main/java/org/zstack/sdk/GpuVendor.java b/sdk/src/main/java/org/zstack/sdk/GpuVendor.java index 0918b690289..22e6066d894 100644 --- a/sdk/src/main/java/org/zstack/sdk/GpuVendor.java +++ b/sdk/src/main/java/org/zstack/sdk/GpuVendor.java @@ -7,6 +7,7 @@ public enum GpuVendor { Haiguang, Huawei, TianShu, + Kunlunxin, Other, Alibaba, } diff --git a/sdk/src/main/java/org/zstack/sdk/LogCategory.java b/sdk/src/main/java/org/zstack/sdk/LogCategory.java index 96662c4bfb3..03ccc56a295 100644 --- a/sdk/src/main/java/org/zstack/sdk/LogCategory.java +++ b/sdk/src/main/java/org/zstack/sdk/LogCategory.java @@ -3,4 +3,5 @@ public enum LogCategory { ManagementNodeLog, PlatformOperationLog, + SnatLog, } diff --git a/sdk/src/main/java/org/zstack/sdk/LongJobProgressDetail.java b/sdk/src/main/java/org/zstack/sdk/LongJobProgressDetail.java new file mode 100644 index 00000000000..788c12fa136 --- /dev/null +++ b/sdk/src/main/java/org/zstack/sdk/LongJobProgressDetail.java @@ -0,0 +1,103 @@ +package org.zstack.sdk; + + + +public class LongJobProgressDetail { + + public java.lang.Integer percent; + public void setPercent(java.lang.Integer percent) { + this.percent = percent; + } + public java.lang.Integer getPercent() { + return this.percent; + } + + public java.lang.String stage; + public void setStage(java.lang.String stage) { + this.stage = stage; + } + public java.lang.String getStage() { + return this.stage; + } + + public java.lang.String state; + public void setState(java.lang.String state) { + this.state = state; + } + public java.lang.String getState() { + return this.state; + } + + public java.lang.String stateReason; + public void setStateReason(java.lang.String stateReason) { + this.stateReason = stateReason; + } + public java.lang.String getStateReason() { + return this.stateReason; + } + + public java.lang.Long processed; + public void setProcessed(java.lang.Long processed) { + this.processed = processed; + } + public java.lang.Long getProcessed() { + return this.processed; + } + + public java.lang.Long total; + public void setTotal(java.lang.Long total) { + this.total = total; + } + public java.lang.Long getTotal() { + return this.total; + } + + public java.lang.Long processedItems; + public void setProcessedItems(java.lang.Long processedItems) { + this.processedItems = processedItems; + } + public java.lang.Long getProcessedItems() { + return this.processedItems; + } + + public java.lang.Long totalItems; + public void setTotalItems(java.lang.Long totalItems) { + this.totalItems = totalItems; + } + public java.lang.Long getTotalItems() { + return this.totalItems; + } + + public java.lang.Long speed; + public void setSpeed(java.lang.Long speed) { + this.speed = speed; + } + public java.lang.Long getSpeed() { + return this.speed; + } + + public java.lang.String unit; + public void setUnit(java.lang.String unit) { + this.unit = unit; + } + public java.lang.String getUnit() { + return this.unit; + } + + public java.lang.Long estimatedRemainingSeconds; + public void setEstimatedRemainingSeconds(java.lang.Long estimatedRemainingSeconds) { + this.estimatedRemainingSeconds = estimatedRemainingSeconds; + } + public java.lang.Long getEstimatedRemainingSeconds() { + return this.estimatedRemainingSeconds; + } + + public java.util.Map extra; + public void setExtra(java.util.Map extra) { + this.extra = extra; + } + public java.util.Map getExtra() { + return this.extra; + } + +} diff --git a/sdk/src/main/java/org/zstack/sdk/MatchModelServiceTemplateWithModelAction.java b/sdk/src/main/java/org/zstack/sdk/MatchModelServiceTemplateWithModelAction.java index 7c0eca70894..36df79c8c7c 100644 --- a/sdk/src/main/java/org/zstack/sdk/MatchModelServiceTemplateWithModelAction.java +++ b/sdk/src/main/java/org/zstack/sdk/MatchModelServiceTemplateWithModelAction.java @@ -64,6 +64,9 @@ public Result throwExceptionIfError() { @Param(required = false, nonempty = false, nullElements = false, emptyString = true, noTrim = false) public java.lang.Integer cpuNum; + @Param(required = false, nonempty = false, nullElements = false, emptyString = true, noTrim = false) + public java.lang.Integer requestCpuNum; + @Param(required = true, nonempty = false, nullElements = false, emptyString = true, noTrim = false) public java.lang.String name; @@ -82,6 +85,9 @@ public Result throwExceptionIfError() { @Param(required = false, nonempty = false, nullElements = false, emptyString = true, noTrim = false) public java.lang.Long memorySize; + @Param(required = false, nonempty = false, nullElements = false, emptyString = true, noTrim = false) + public java.lang.Long requestMemorySize; + @Param(required = false, nonempty = false, nullElements = false, emptyString = true, noTrim = false) public java.util.List l3NetworkUuids; diff --git a/sdk/src/main/java/org/zstack/sdk/MdevDeviceInventory.java b/sdk/src/main/java/org/zstack/sdk/MdevDeviceInventory.java index e5f8c6e593a..1d8b418e2be 100644 --- a/sdk/src/main/java/org/zstack/sdk/MdevDeviceInventory.java +++ b/sdk/src/main/java/org/zstack/sdk/MdevDeviceInventory.java @@ -127,4 +127,12 @@ public java.lang.String getVendor() { return this.vendor; } + public java.lang.String shareType; + public void setShareType(java.lang.String shareType) { + this.shareType = shareType; + } + public java.lang.String getShareType() { + return this.shareType; + } + } diff --git a/sdk/src/main/java/org/zstack/sdk/MdevDeviceSpecInventory.java b/sdk/src/main/java/org/zstack/sdk/MdevDeviceSpecInventory.java index 4bb5364a115..bdb948d4ef0 100644 --- a/sdk/src/main/java/org/zstack/sdk/MdevDeviceSpecInventory.java +++ b/sdk/src/main/java/org/zstack/sdk/MdevDeviceSpecInventory.java @@ -93,4 +93,12 @@ public java.lang.Integer getMaxAvailableDevicesPerHost() { return this.maxAvailableDevicesPerHost; } + public java.lang.String shareType; + public void setShareType(java.lang.String shareType) { + this.shareType = shareType; + } + public java.lang.String getShareType() { + return this.shareType; + } + } diff --git a/sdk/src/main/java/org/zstack/sdk/ModelService.java b/sdk/src/main/java/org/zstack/sdk/ModelService.java index 32cb11e9495..6f7bb93c223 100644 --- a/sdk/src/main/java/org/zstack/sdk/ModelService.java +++ b/sdk/src/main/java/org/zstack/sdk/ModelService.java @@ -108,6 +108,14 @@ public java.lang.Integer getCpuNum() { return this.cpuNum; } + public java.lang.Integer requestCpuNum; + public void setRequestCpuNum(java.lang.Integer requestCpuNum) { + this.requestCpuNum = requestCpuNum; + } + public java.lang.Integer getRequestCpuNum() { + return this.requestCpuNum; + } + public java.lang.String name; public void setName(java.lang.String name) { this.name = name; @@ -156,6 +164,14 @@ public java.lang.Long getMemorySize() { return this.memorySize; } + public java.lang.Long requestMemorySize; + public void setRequestMemorySize(java.lang.Long requestMemorySize) { + this.requestMemorySize = requestMemorySize; + } + public java.lang.Long getRequestMemorySize() { + return this.requestMemorySize; + } + public java.util.List l3NetworkUuids; public void setL3NetworkUuids(java.util.List l3NetworkUuids) { this.l3NetworkUuids = l3NetworkUuids; diff --git a/sdk/src/main/java/org/zstack/sdk/MountModelToVmInstanceAction.java b/sdk/src/main/java/org/zstack/sdk/MountModelToVmInstanceAction.java new file mode 100644 index 00000000000..9dfca7a35d1 --- /dev/null +++ b/sdk/src/main/java/org/zstack/sdk/MountModelToVmInstanceAction.java @@ -0,0 +1,107 @@ +package org.zstack.sdk; + +import java.util.HashMap; +import java.util.Map; +import org.zstack.sdk.*; + +public class MountModelToVmInstanceAction 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.MountModelToVmInstanceResult 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 = false, nullElements = false, emptyString = true, noTrim = false) + public java.lang.String vmInstanceUuid; + + @Param(required = true, nonempty = false, nullElements = false, emptyString = true, noTrim = false) + public java.lang.String modelUuid; + + @Param(required = false, maxLength = 512, nonempty = false, nullElements = false, emptyString = true, noTrim = false) + public java.lang.String mountPath; + + @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.MountModelToVmInstanceResult value = res.getResult(org.zstack.sdk.MountModelToVmInstanceResult.class); + ret.value = value == null ? new org.zstack.sdk.MountModelToVmInstanceResult() : 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 = "/vm-model-mounts"; + info.needSession = true; + info.needPoll = true; + info.parameterName = "params"; + return info; + } + +} diff --git a/sdk/src/main/java/org/zstack/sdk/MountModelToVmInstanceResult.java b/sdk/src/main/java/org/zstack/sdk/MountModelToVmInstanceResult.java new file mode 100644 index 00000000000..0feb080e656 --- /dev/null +++ b/sdk/src/main/java/org/zstack/sdk/MountModelToVmInstanceResult.java @@ -0,0 +1,14 @@ +package org.zstack.sdk; + +import org.zstack.sdk.VmModelMountInventory; + +public class MountModelToVmInstanceResult { + public VmModelMountInventory inventory; + public void setInventory(VmModelMountInventory inventory) { + this.inventory = inventory; + } + public VmModelMountInventory getInventory() { + return this.inventory; + } + +} diff --git a/sdk/src/main/java/org/zstack/sdk/PciDeviceInventory.java b/sdk/src/main/java/org/zstack/sdk/PciDeviceInventory.java index a695b6c1b26..e5cc89cce7d 100644 --- a/sdk/src/main/java/org/zstack/sdk/PciDeviceInventory.java +++ b/sdk/src/main/java/org/zstack/sdk/PciDeviceInventory.java @@ -4,6 +4,8 @@ import org.zstack.sdk.PciDeviceState; import org.zstack.sdk.PciDeviceStatus; import org.zstack.sdk.PciDeviceVirtStatus; +import org.zstack.sdk.PciDeviceVirtState; +import org.zstack.sdk.PciDeviceVirtMode; import org.zstack.sdk.PciDeviceChooser; import org.zstack.sdk.PciDeviceMetaData; @@ -97,6 +99,30 @@ public PciDeviceVirtStatus getVirtStatus() { return this.virtStatus; } + public PciDeviceVirtState virtState; + public void setVirtState(PciDeviceVirtState virtState) { + this.virtState = virtState; + } + public PciDeviceVirtState getVirtState() { + return this.virtState; + } + + public java.util.List virtCapabilities; + public void setVirtCapabilities(java.util.List virtCapabilities) { + this.virtCapabilities = virtCapabilities; + } + public java.util.List getVirtCapabilities() { + return this.virtCapabilities; + } + + public PciDeviceVirtMode virtMode; + public void setVirtMode(PciDeviceVirtMode virtMode) { + this.virtMode = virtMode; + } + public PciDeviceVirtMode getVirtMode() { + return this.virtMode; + } + public PciDeviceChooser chooser; public void setChooser(PciDeviceChooser chooser) { this.chooser = chooser; @@ -233,4 +259,12 @@ public java.util.List getMdevSpecRefs() { return this.mdevSpecRefs; } + public java.lang.String shareType; + public void setShareType(java.lang.String shareType) { + this.shareType = shareType; + } + public java.lang.String getShareType() { + return this.shareType; + } + } diff --git a/sdk/src/main/java/org/zstack/sdk/PciDeviceSpecInventory.java b/sdk/src/main/java/org/zstack/sdk/PciDeviceSpecInventory.java index 6bead4ce0d9..3f2c33ca2de 100644 --- a/sdk/src/main/java/org/zstack/sdk/PciDeviceSpecInventory.java +++ b/sdk/src/main/java/org/zstack/sdk/PciDeviceSpecInventory.java @@ -165,4 +165,12 @@ public java.lang.Integer getMaxAvailableDevicesPerHost() { return this.maxAvailableDevicesPerHost; } + public java.lang.String shareType; + public void setShareType(java.lang.String shareType) { + this.shareType = shareType; + } + public java.lang.String getShareType() { + return this.shareType; + } + } diff --git a/sdk/src/main/java/org/zstack/sdk/PciDeviceVirtMode.java b/sdk/src/main/java/org/zstack/sdk/PciDeviceVirtMode.java new file mode 100644 index 00000000000..f9fcb6a79aa --- /dev/null +++ b/sdk/src/main/java/org/zstack/sdk/PciDeviceVirtMode.java @@ -0,0 +1,9 @@ +package org.zstack.sdk; + +public enum PciDeviceVirtMode { + SRIOV, + VFIO_MDEV, + K8S_BYPASS, + TENSORFUSION, + HAMI, +} diff --git a/sdk/src/main/java/org/zstack/sdk/PciDeviceVirtState.java b/sdk/src/main/java/org/zstack/sdk/PciDeviceVirtState.java new file mode 100644 index 00000000000..7a56eabf9c0 --- /dev/null +++ b/sdk/src/main/java/org/zstack/sdk/PciDeviceVirtState.java @@ -0,0 +1,8 @@ +package org.zstack.sdk; + +public enum PciDeviceVirtState { + UNVIRTUALIZABLE, + VIRTUALIZABLE, + VIRTUALIZED, + VIRTUAL, +} diff --git a/sdk/src/main/java/org/zstack/sdk/PciDeviceVirtStatus.java b/sdk/src/main/java/org/zstack/sdk/PciDeviceVirtStatus.java index e59f4237d25..3612d6fb754 100644 --- a/sdk/src/main/java/org/zstack/sdk/PciDeviceVirtStatus.java +++ b/sdk/src/main/java/org/zstack/sdk/PciDeviceVirtStatus.java @@ -9,5 +9,7 @@ public enum PciDeviceVirtStatus { SRIOV_VIRTUAL, VIRTUALIZED_BYPASS_ZSTACK, HAMI_VIRTUALIZED, + TENSORFUSION_VIRTUALIZABLE, + TENSORFUSION_VIRTUALIZED, UNKNOWN, } diff --git a/sdk/src/main/java/org/zstack/sdk/QueryDGpuDeviceAction.java b/sdk/src/main/java/org/zstack/sdk/QueryDGpuDeviceAction.java new file mode 100644 index 00000000000..ce5fc12e4af --- /dev/null +++ b/sdk/src/main/java/org/zstack/sdk/QueryDGpuDeviceAction.java @@ -0,0 +1,75 @@ +package org.zstack.sdk; + +import java.util.HashMap; +import java.util.Map; +import org.zstack.sdk.*; + +public class QueryDGpuDeviceAction extends QueryAction { + + 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.QueryDGpuDeviceResult 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; + } + } + + + + private Result makeResult(ApiResult res) { + Result ret = new Result(); + if (res.error != null) { + ret.error = res.error; + return ret; + } + + org.zstack.sdk.QueryDGpuDeviceResult value = res.getResult(org.zstack.sdk.QueryDGpuDeviceResult.class); + ret.value = value == null ? new org.zstack.sdk.QueryDGpuDeviceResult() : 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 = "GET"; + info.path = "/gpu-device/dgpu-devices"; + info.needSession = true; + info.needPoll = false; + info.parameterName = ""; + return info; + } + +} diff --git a/sdk/src/main/java/org/zstack/sdk/QueryDGpuDeviceResult.java b/sdk/src/main/java/org/zstack/sdk/QueryDGpuDeviceResult.java new file mode 100644 index 00000000000..54bbf450e4b --- /dev/null +++ b/sdk/src/main/java/org/zstack/sdk/QueryDGpuDeviceResult.java @@ -0,0 +1,22 @@ +package org.zstack.sdk; + + + +public class QueryDGpuDeviceResult { + public java.util.List inventories; + public void setInventories(java.util.List inventories) { + this.inventories = inventories; + } + public java.util.List getInventories() { + return this.inventories; + } + + public java.lang.Long total; + public void setTotal(java.lang.Long total) { + this.total = total; + } + public java.lang.Long getTotal() { + return this.total; + } + +} diff --git a/sdk/src/main/java/org/zstack/sdk/QueryDGpuProfileAction.java b/sdk/src/main/java/org/zstack/sdk/QueryDGpuProfileAction.java new file mode 100644 index 00000000000..b0f5b38380c --- /dev/null +++ b/sdk/src/main/java/org/zstack/sdk/QueryDGpuProfileAction.java @@ -0,0 +1,75 @@ +package org.zstack.sdk; + +import java.util.HashMap; +import java.util.Map; +import org.zstack.sdk.*; + +public class QueryDGpuProfileAction extends QueryAction { + + 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.QueryDGpuProfileResult 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; + } + } + + + + private Result makeResult(ApiResult res) { + Result ret = new Result(); + if (res.error != null) { + ret.error = res.error; + return ret; + } + + org.zstack.sdk.QueryDGpuProfileResult value = res.getResult(org.zstack.sdk.QueryDGpuProfileResult.class); + ret.value = value == null ? new org.zstack.sdk.QueryDGpuProfileResult() : 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 = "GET"; + info.path = "/gpu-device/dgpu-profiles"; + info.needSession = true; + info.needPoll = false; + info.parameterName = ""; + return info; + } + +} diff --git a/sdk/src/main/java/org/zstack/sdk/QueryDGpuProfileResult.java b/sdk/src/main/java/org/zstack/sdk/QueryDGpuProfileResult.java new file mode 100644 index 00000000000..7be7fc6e021 --- /dev/null +++ b/sdk/src/main/java/org/zstack/sdk/QueryDGpuProfileResult.java @@ -0,0 +1,22 @@ +package org.zstack.sdk; + + + +public class QueryDGpuProfileResult { + public java.util.List inventories; + public void setInventories(java.util.List inventories) { + this.inventories = inventories; + } + public java.util.List getInventories() { + return this.inventories; + } + + public java.lang.Long total; + public void setTotal(java.lang.Long total) { + this.total = total; + } + public java.lang.Long getTotal() { + return this.total; + } + +} diff --git a/sdk/src/main/java/org/zstack/sdk/QueryExternalServiceConfigurationAction.java b/sdk/src/main/java/org/zstack/sdk/QueryExternalServiceConfigurationAction.java new file mode 100644 index 00000000000..069f3a8770f --- /dev/null +++ b/sdk/src/main/java/org/zstack/sdk/QueryExternalServiceConfigurationAction.java @@ -0,0 +1,75 @@ +package org.zstack.sdk; + +import java.util.HashMap; +import java.util.Map; +import org.zstack.sdk.*; + +public class QueryExternalServiceConfigurationAction extends QueryAction { + + 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.QueryExternalServiceConfigurationResult 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; + } + } + + + + private Result makeResult(ApiResult res) { + Result ret = new Result(); + if (res.error != null) { + ret.error = res.error; + return ret; + } + + org.zstack.sdk.QueryExternalServiceConfigurationResult value = res.getResult(org.zstack.sdk.QueryExternalServiceConfigurationResult.class); + ret.value = value == null ? new org.zstack.sdk.QueryExternalServiceConfigurationResult() : 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 = "GET"; + info.path = "/external/service/configuration"; + info.needSession = true; + info.needPoll = false; + info.parameterName = ""; + return info; + } + +} diff --git a/sdk/src/main/java/org/zstack/sdk/QueryExternalServiceConfigurationResult.java b/sdk/src/main/java/org/zstack/sdk/QueryExternalServiceConfigurationResult.java new file mode 100644 index 00000000000..4697f0cc1fb --- /dev/null +++ b/sdk/src/main/java/org/zstack/sdk/QueryExternalServiceConfigurationResult.java @@ -0,0 +1,22 @@ +package org.zstack.sdk; + + + +public class QueryExternalServiceConfigurationResult { + public java.util.List inventories; + public void setInventories(java.util.List inventories) { + this.inventories = inventories; + } + public java.util.List getInventories() { + return this.inventories; + } + + public java.lang.Long total; + public void setTotal(java.lang.Long total) { + this.total = total; + } + public java.lang.Long getTotal() { + return this.total; + } + +} diff --git a/sdk/src/main/java/org/zstack/sdk/QueryVmModelMountAction.java b/sdk/src/main/java/org/zstack/sdk/QueryVmModelMountAction.java new file mode 100644 index 00000000000..3205a750d34 --- /dev/null +++ b/sdk/src/main/java/org/zstack/sdk/QueryVmModelMountAction.java @@ -0,0 +1,75 @@ +package org.zstack.sdk; + +import java.util.HashMap; +import java.util.Map; +import org.zstack.sdk.*; + +public class QueryVmModelMountAction extends QueryAction { + + 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.QueryVmModelMountResult 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; + } + } + + + + private Result makeResult(ApiResult res) { + Result ret = new Result(); + if (res.error != null) { + ret.error = res.error; + return ret; + } + + org.zstack.sdk.QueryVmModelMountResult value = res.getResult(org.zstack.sdk.QueryVmModelMountResult.class); + ret.value = value == null ? new org.zstack.sdk.QueryVmModelMountResult() : 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 = "GET"; + info.path = "/vm-model-mounts"; + info.needSession = true; + info.needPoll = false; + info.parameterName = ""; + return info; + } + +} diff --git a/sdk/src/main/java/org/zstack/sdk/QueryVmModelMountResult.java b/sdk/src/main/java/org/zstack/sdk/QueryVmModelMountResult.java new file mode 100644 index 00000000000..de488a4a8a0 --- /dev/null +++ b/sdk/src/main/java/org/zstack/sdk/QueryVmModelMountResult.java @@ -0,0 +1,22 @@ +package org.zstack.sdk; + + + +public class QueryVmModelMountResult { + public java.util.List inventories; + public void setInventories(java.util.List inventories) { + this.inventories = inventories; + } + public java.util.List getInventories() { + return this.inventories; + } + + public java.lang.Long total; + public void setTotal(java.lang.Long total) { + this.total = total; + } + public java.lang.Long getTotal() { + return this.total; + } + +} diff --git a/sdk/src/main/java/org/zstack/sdk/RemoveVmDGpuStrategyAction.java b/sdk/src/main/java/org/zstack/sdk/RemoveVmDGpuStrategyAction.java new file mode 100644 index 00000000000..e0d74840df8 --- /dev/null +++ b/sdk/src/main/java/org/zstack/sdk/RemoveVmDGpuStrategyAction.java @@ -0,0 +1,101 @@ +package org.zstack.sdk; + +import java.util.HashMap; +import java.util.Map; +import org.zstack.sdk.*; + +public class RemoveVmDGpuStrategyAction 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.RemoveVmDGpuStrategyResult 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 = false, nullElements = false, emptyString = true, noTrim = false) + public java.lang.String vmInstanceUuid; + + @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.RemoveVmDGpuStrategyResult value = res.getResult(org.zstack.sdk.RemoveVmDGpuStrategyResult.class); + ret.value = value == null ? new org.zstack.sdk.RemoveVmDGpuStrategyResult() : 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 = "DELETE"; + info.path = "/vm-instances/{vmInstanceUuid}/dgpu-strategy"; + info.needSession = true; + info.needPoll = true; + info.parameterName = ""; + return info; + } + +} diff --git a/sdk/src/main/java/org/zstack/sdk/RemoveVmDGpuStrategyResult.java b/sdk/src/main/java/org/zstack/sdk/RemoveVmDGpuStrategyResult.java new file mode 100644 index 00000000000..866bc7622dc --- /dev/null +++ b/sdk/src/main/java/org/zstack/sdk/RemoveVmDGpuStrategyResult.java @@ -0,0 +1,7 @@ +package org.zstack.sdk; + + + +public class RemoveVmDGpuStrategyResult { + +} diff --git a/sdk/src/main/java/org/zstack/sdk/SetDGpuProfileAction.java b/sdk/src/main/java/org/zstack/sdk/SetDGpuProfileAction.java new file mode 100644 index 00000000000..c0fedea7396 --- /dev/null +++ b/sdk/src/main/java/org/zstack/sdk/SetDGpuProfileAction.java @@ -0,0 +1,107 @@ +package org.zstack.sdk; + +import java.util.HashMap; +import java.util.Map; +import org.zstack.sdk.*; + +public class SetDGpuProfileAction 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.SetDGpuProfileResult 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 = false, nullElements = false, emptyString = true, noTrim = false) + public java.lang.String gpuSpecUuid; + + @Param(required = true, nonempty = true, nullElements = false, emptyString = true, noTrim = false) + public java.util.List memorySizes; + + @Param(required = false, nonempty = false, nullElements = false, emptyString = true, noTrim = false) + public java.lang.Long shmemSize; + + @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.SetDGpuProfileResult value = res.getResult(org.zstack.sdk.SetDGpuProfileResult.class); + ret.value = value == null ? new org.zstack.sdk.SetDGpuProfileResult() : 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 = "PUT"; + info.path = "/gpu-device/gpu-device-specs/{gpuSpecUuid}/actions"; + info.needSession = true; + info.needPoll = true; + info.parameterName = "setDGpuProfile"; + return info; + } + +} diff --git a/sdk/src/main/java/org/zstack/sdk/SetDGpuProfileResult.java b/sdk/src/main/java/org/zstack/sdk/SetDGpuProfileResult.java new file mode 100644 index 00000000000..6dc9258fdb7 --- /dev/null +++ b/sdk/src/main/java/org/zstack/sdk/SetDGpuProfileResult.java @@ -0,0 +1,14 @@ +package org.zstack.sdk; + + + +public class SetDGpuProfileResult { + public java.util.List inventories; + public void setInventories(java.util.List inventories) { + this.inventories = inventories; + } + public java.util.List getInventories() { + return this.inventories; + } + +} diff --git a/sdk/src/main/java/org/zstack/sdk/SetVmDGpuStrategyAction.java b/sdk/src/main/java/org/zstack/sdk/SetVmDGpuStrategyAction.java new file mode 100644 index 00000000000..e8f773e19de --- /dev/null +++ b/sdk/src/main/java/org/zstack/sdk/SetVmDGpuStrategyAction.java @@ -0,0 +1,113 @@ +package org.zstack.sdk; + +import java.util.HashMap; +import java.util.Map; +import org.zstack.sdk.*; + +public class SetVmDGpuStrategyAction 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.SetVmDGpuStrategyResult 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 = false, nullElements = false, emptyString = true, noTrim = false) + public java.lang.String vmInstanceUuid; + + @Param(required = true, nonempty = false, nullElements = false, emptyString = true, noTrim = false) + public java.lang.String dgpuProfileUuid; + + @Param(required = false, nonempty = false, nullElements = false, emptyString = true, noTrim = false) + public java.lang.String gpuDeviceUuid; + + @Param(required = true, validValues = {"BySpec","ByDevice"}, nonempty = false, nullElements = false, emptyString = true, noTrim = false) + public java.lang.String chooser; + + @Param(required = false, nonempty = false, nullElements = false, emptyString = true, noTrim = false) + public java.lang.Boolean autoDetachOnStop; + + @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.SetVmDGpuStrategyResult value = res.getResult(org.zstack.sdk.SetVmDGpuStrategyResult.class); + ret.value = value == null ? new org.zstack.sdk.SetVmDGpuStrategyResult() : 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 = "PUT"; + info.path = "/vm-instances/{vmInstanceUuid}/actions"; + info.needSession = true; + info.needPoll = true; + info.parameterName = "setVmDGpuStrategy"; + return info; + } + +} diff --git a/sdk/src/main/java/org/zstack/sdk/SetVmDGpuStrategyResult.java b/sdk/src/main/java/org/zstack/sdk/SetVmDGpuStrategyResult.java new file mode 100644 index 00000000000..d7815916628 --- /dev/null +++ b/sdk/src/main/java/org/zstack/sdk/SetVmDGpuStrategyResult.java @@ -0,0 +1,7 @@ +package org.zstack.sdk; + + + +public class SetVmDGpuStrategyResult { + +} diff --git a/sdk/src/main/java/org/zstack/sdk/SetVmStaticIpAction.java b/sdk/src/main/java/org/zstack/sdk/SetVmStaticIpAction.java index 8240899d354..3deb766e3ce 100644 --- a/sdk/src/main/java/org/zstack/sdk/SetVmStaticIpAction.java +++ b/sdk/src/main/java/org/zstack/sdk/SetVmStaticIpAction.java @@ -49,6 +49,9 @@ public Result throwExceptionIfError() { @Param(required = false, nonempty = false, nullElements = false, emptyString = true, noTrim = false) public java.lang.String ipv6Prefix; + @Param(required = false, nonempty = false, nullElements = false, emptyString = true, noTrim = false) + public java.util.List dnsAddresses; + @Param(required = false) public java.util.List systemTags; diff --git a/sdk/src/main/java/org/zstack/sdk/SetVpcVRouterSnatLogStateAction.java b/sdk/src/main/java/org/zstack/sdk/SetVpcVRouterSnatLogStateAction.java new file mode 100644 index 00000000000..8628bf66021 --- /dev/null +++ b/sdk/src/main/java/org/zstack/sdk/SetVpcVRouterSnatLogStateAction.java @@ -0,0 +1,104 @@ +package org.zstack.sdk; + +import java.util.HashMap; +import java.util.Map; +import org.zstack.sdk.*; + +public class SetVpcVRouterSnatLogStateAction 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.SetVpcVRouterSnatLogStateResult 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 = false, nullElements = false, emptyString = true, noTrim = false) + public java.lang.String uuid; + + @Param(required = true, validValues = {"enable","disable"}, nonempty = false, nullElements = false, emptyString = true, noTrim = false) + public java.lang.String state; + + @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.SetVpcVRouterSnatLogStateResult value = res.getResult(org.zstack.sdk.SetVpcVRouterSnatLogStateResult.class); + ret.value = value == null ? new org.zstack.sdk.SetVpcVRouterSnatLogStateResult() : 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 = "PUT"; + info.path = "/vpc/virtual-routers/{uuid}/snat-log"; + info.needSession = true; + info.needPoll = true; + info.parameterName = "setVpcVRouterSnatLogState"; + return info; + } + +} diff --git a/sdk/src/main/java/org/zstack/sdk/SetVpcVRouterSnatLogStateResult.java b/sdk/src/main/java/org/zstack/sdk/SetVpcVRouterSnatLogStateResult.java new file mode 100644 index 00000000000..41da7da65ba --- /dev/null +++ b/sdk/src/main/java/org/zstack/sdk/SetVpcVRouterSnatLogStateResult.java @@ -0,0 +1,14 @@ +package org.zstack.sdk; + + + +public class SetVpcVRouterSnatLogStateResult { + public java.lang.String state; + public void setState(java.lang.String state) { + this.state = state; + } + public java.lang.String getState() { + return this.state; + } + +} diff --git a/sdk/src/main/java/org/zstack/sdk/StartBareMetal2InstanceAction.java b/sdk/src/main/java/org/zstack/sdk/StartBareMetal2InstanceAction.java index 4563ac62512..7b9f3459f9a 100644 --- a/sdk/src/main/java/org/zstack/sdk/StartBareMetal2InstanceAction.java +++ b/sdk/src/main/java/org/zstack/sdk/StartBareMetal2InstanceAction.java @@ -40,6 +40,9 @@ public Result throwExceptionIfError() { @Param(required = false, nonempty = false, nullElements = false, emptyString = true, noTrim = false) public java.lang.String chassisOfferingUuid; + @Param(required = false, validValues = {"IPMI","DPU"}, nonempty = false, nullElements = false, emptyString = true, noTrim = false) + public java.lang.String chassisType; + @Param(required = false) public java.util.List systemTags; diff --git a/sdk/src/main/java/org/zstack/sdk/TagPatternInventory.java b/sdk/src/main/java/org/zstack/sdk/TagPatternInventory.java index d430eeb2b34..141f8dec363 100644 --- a/sdk/src/main/java/org/zstack/sdk/TagPatternInventory.java +++ b/sdk/src/main/java/org/zstack/sdk/TagPatternInventory.java @@ -52,6 +52,14 @@ public TagPatternType getType() { return this.type; } + public java.lang.String resourceType; + public void setResourceType(java.lang.String resourceType) { + this.resourceType = resourceType; + } + public java.lang.String getResourceType() { + return this.resourceType; + } + public java.sql.Timestamp createDate; public void setCreateDate(java.sql.Timestamp createDate) { this.createDate = createDate; diff --git a/sdk/src/main/java/org/zstack/sdk/TaskProgressInventory.java b/sdk/src/main/java/org/zstack/sdk/TaskProgressInventory.java index 31d427d23d4..6bec468e66f 100644 --- a/sdk/src/main/java/org/zstack/sdk/TaskProgressInventory.java +++ b/sdk/src/main/java/org/zstack/sdk/TaskProgressInventory.java @@ -1,6 +1,6 @@ package org.zstack.sdk; - +import org.zstack.sdk.LongJobProgressDetail; public class TaskProgressInventory { @@ -76,4 +76,12 @@ public java.lang.String getArguments() { return this.arguments; } + public LongJobProgressDetail progressDetail; + public void setProgressDetail(LongJobProgressDetail progressDetail) { + this.progressDetail = progressDetail; + } + public LongJobProgressDetail getProgressDetail() { + return this.progressDetail; + } + } diff --git a/sdk/src/main/java/org/zstack/sdk/UnmountModelFromVmInstanceAction.java b/sdk/src/main/java/org/zstack/sdk/UnmountModelFromVmInstanceAction.java new file mode 100644 index 00000000000..677cd311888 --- /dev/null +++ b/sdk/src/main/java/org/zstack/sdk/UnmountModelFromVmInstanceAction.java @@ -0,0 +1,101 @@ +package org.zstack.sdk; + +import java.util.HashMap; +import java.util.Map; +import org.zstack.sdk.*; + +public class UnmountModelFromVmInstanceAction 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.UnmountModelFromVmInstanceResult 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 = false, nullElements = false, emptyString = true, noTrim = false) + public java.lang.String uuid; + + @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.UnmountModelFromVmInstanceResult value = res.getResult(org.zstack.sdk.UnmountModelFromVmInstanceResult.class); + ret.value = value == null ? new org.zstack.sdk.UnmountModelFromVmInstanceResult() : 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 = "DELETE"; + info.path = "/vm-model-mounts/{uuid}"; + info.needSession = true; + info.needPoll = true; + info.parameterName = ""; + return info; + } + +} diff --git a/sdk/src/main/java/org/zstack/sdk/UnmountModelFromVmInstanceResult.java b/sdk/src/main/java/org/zstack/sdk/UnmountModelFromVmInstanceResult.java new file mode 100644 index 00000000000..736cdec965f --- /dev/null +++ b/sdk/src/main/java/org/zstack/sdk/UnmountModelFromVmInstanceResult.java @@ -0,0 +1,7 @@ +package org.zstack.sdk; + + + +public class UnmountModelFromVmInstanceResult { + +} diff --git a/sdk/src/main/java/org/zstack/sdk/UpdateExternalServiceConfigurationAction.java b/sdk/src/main/java/org/zstack/sdk/UpdateExternalServiceConfigurationAction.java new file mode 100644 index 00000000000..c91d30ed13d --- /dev/null +++ b/sdk/src/main/java/org/zstack/sdk/UpdateExternalServiceConfigurationAction.java @@ -0,0 +1,104 @@ +package org.zstack.sdk; + +import java.util.HashMap; +import java.util.Map; +import org.zstack.sdk.*; + +public class UpdateExternalServiceConfigurationAction 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.UpdateExternalServiceConfigurationResult 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, maxLength = 32, nonempty = false, nullElements = false, emptyString = true, noTrim = false) + public java.lang.String uuid; + + @Param(required = false, maxLength = 2048, nonempty = false, nullElements = false, emptyString = true, noTrim = false) + public java.lang.String description; + + @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.UpdateExternalServiceConfigurationResult value = res.getResult(org.zstack.sdk.UpdateExternalServiceConfigurationResult.class); + ret.value = value == null ? new org.zstack.sdk.UpdateExternalServiceConfigurationResult() : 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 = "PUT"; + info.path = "/external/service/configuration/{uuid}"; + info.needSession = true; + info.needPoll = true; + info.parameterName = "updateExternalServiceConfiguration"; + return info; + } + +} diff --git a/sdk/src/main/java/org/zstack/sdk/UpdateExternalServiceConfigurationResult.java b/sdk/src/main/java/org/zstack/sdk/UpdateExternalServiceConfigurationResult.java new file mode 100644 index 00000000000..e00d4bc9fcb --- /dev/null +++ b/sdk/src/main/java/org/zstack/sdk/UpdateExternalServiceConfigurationResult.java @@ -0,0 +1,14 @@ +package org.zstack.sdk; + +import org.zstack.sdk.ExternalServiceConfigurationInventory; + +public class UpdateExternalServiceConfigurationResult { + public ExternalServiceConfigurationInventory inventory; + public void setInventory(ExternalServiceConfigurationInventory inventory) { + this.inventory = inventory; + } + public ExternalServiceConfigurationInventory getInventory() { + return this.inventory; + } + +} diff --git a/sdk/src/main/java/org/zstack/sdk/UpdateVmInstancePciDeviceSpecRefAction.java b/sdk/src/main/java/org/zstack/sdk/UpdateVmInstancePciDeviceSpecRefAction.java new file mode 100644 index 00000000000..41277b9a48a --- /dev/null +++ b/sdk/src/main/java/org/zstack/sdk/UpdateVmInstancePciDeviceSpecRefAction.java @@ -0,0 +1,107 @@ +package org.zstack.sdk; + +import java.util.HashMap; +import java.util.Map; +import org.zstack.sdk.*; + +public class UpdateVmInstancePciDeviceSpecRefAction 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.UpdateVmInstancePciDeviceSpecRefResult 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 = false, nullElements = false, emptyString = true, noTrim = false) + public java.lang.String pciSpecUuid; + + @Param(required = true, nonempty = false, nullElements = false, emptyString = true, noTrim = false) + public java.lang.String vmInstanceUuid; + + @Param(required = true, nonempty = false, nullElements = false, emptyString = true, numberRange = {1L,100L}, noTrim = false) + public int pciDeviceNumber = 0; + + @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.UpdateVmInstancePciDeviceSpecRefResult value = res.getResult(org.zstack.sdk.UpdateVmInstancePciDeviceSpecRefResult.class); + ret.value = value == null ? new org.zstack.sdk.UpdateVmInstancePciDeviceSpecRefResult() : 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 = "PUT"; + info.path = "/pci-device-specs/{pciSpecUuid}/vm-instances/{vmInstanceUuid}/actions"; + info.needSession = true; + info.needPoll = true; + info.parameterName = "updateVmInstancePciDeviceSpecRef"; + return info; + } + +} diff --git a/sdk/src/main/java/org/zstack/sdk/UpdateVmInstancePciDeviceSpecRefResult.java b/sdk/src/main/java/org/zstack/sdk/UpdateVmInstancePciDeviceSpecRefResult.java new file mode 100644 index 00000000000..cb771fa0a73 --- /dev/null +++ b/sdk/src/main/java/org/zstack/sdk/UpdateVmInstancePciDeviceSpecRefResult.java @@ -0,0 +1,14 @@ +package org.zstack.sdk; + +import org.zstack.sdk.VmInstancePciDeviceSpecRefInventory; + +public class UpdateVmInstancePciDeviceSpecRefResult { + public VmInstancePciDeviceSpecRefInventory inventory; + public void setInventory(VmInstancePciDeviceSpecRefInventory inventory) { + this.inventory = inventory; + } + public VmInstancePciDeviceSpecRefInventory getInventory() { + return this.inventory; + } + +} diff --git a/sdk/src/main/java/org/zstack/sdk/UsedIpInventory.java b/sdk/src/main/java/org/zstack/sdk/UsedIpInventory.java index 8befbe48ff7..5c47e90bb46 100644 --- a/sdk/src/main/java/org/zstack/sdk/UsedIpInventory.java +++ b/sdk/src/main/java/org/zstack/sdk/UsedIpInventory.java @@ -52,6 +52,14 @@ public java.lang.String getNetmask() { return this.netmask; } + public java.lang.Integer prefixLen; + public void setPrefixLen(java.lang.Integer prefixLen) { + this.prefixLen = prefixLen; + } + public java.lang.Integer getPrefixLen() { + return this.prefixLen; + } + public java.lang.String gateway; public void setGateway(java.lang.String gateway) { this.gateway = gateway; diff --git a/sdk/src/main/java/org/zstack/sdk/VmModelMountInventory.java b/sdk/src/main/java/org/zstack/sdk/VmModelMountInventory.java new file mode 100644 index 00000000000..e328b9292b4 --- /dev/null +++ b/sdk/src/main/java/org/zstack/sdk/VmModelMountInventory.java @@ -0,0 +1,87 @@ +package org.zstack.sdk; + +import org.zstack.sdk.VmModelMountStatus; + +public class VmModelMountInventory { + + public java.lang.String uuid; + public void setUuid(java.lang.String uuid) { + this.uuid = uuid; + } + public java.lang.String getUuid() { + return this.uuid; + } + + public java.lang.String vmInstanceUuid; + public void setVmInstanceUuid(java.lang.String vmInstanceUuid) { + this.vmInstanceUuid = vmInstanceUuid; + } + public java.lang.String getVmInstanceUuid() { + return this.vmInstanceUuid; + } + + public java.lang.String modelUuid; + public void setModelUuid(java.lang.String modelUuid) { + this.modelUuid = modelUuid; + } + public java.lang.String getModelUuid() { + return this.modelUuid; + } + + public java.lang.String modelName; + public void setModelName(java.lang.String modelName) { + this.modelName = modelName; + } + public java.lang.String getModelName() { + return this.modelName; + } + + public java.lang.String mountPath; + public void setMountPath(java.lang.String mountPath) { + this.mountPath = mountPath; + } + public java.lang.String getMountPath() { + return this.mountPath; + } + + public java.lang.String sourcePath; + public void setSourcePath(java.lang.String sourcePath) { + this.sourcePath = sourcePath; + } + public java.lang.String getSourcePath() { + return this.sourcePath; + } + + public VmModelMountStatus status; + public void setStatus(VmModelMountStatus status) { + this.status = status; + } + public VmModelMountStatus getStatus() { + return this.status; + } + + public java.lang.String accountUuid; + public void setAccountUuid(java.lang.String accountUuid) { + this.accountUuid = accountUuid; + } + public java.lang.String getAccountUuid() { + return this.accountUuid; + } + + public java.sql.Timestamp createDate; + public void setCreateDate(java.sql.Timestamp createDate) { + this.createDate = createDate; + } + public java.sql.Timestamp getCreateDate() { + return this.createDate; + } + + public java.sql.Timestamp lastOpDate; + public void setLastOpDate(java.sql.Timestamp lastOpDate) { + this.lastOpDate = lastOpDate; + } + public java.sql.Timestamp getLastOpDate() { + return this.lastOpDate; + } + +} diff --git a/sdk/src/main/java/org/zstack/sdk/VmModelMountStatus.java b/sdk/src/main/java/org/zstack/sdk/VmModelMountStatus.java new file mode 100644 index 00000000000..67222f08987 --- /dev/null +++ b/sdk/src/main/java/org/zstack/sdk/VmModelMountStatus.java @@ -0,0 +1,6 @@ +package org.zstack.sdk; + +public enum VmModelMountStatus { + Mounted, + Unmounting, +} diff --git a/sdk/src/main/java/org/zstack/sdk/ZSClient.java b/sdk/src/main/java/org/zstack/sdk/ZSClient.java index 650369b19ac..3cab7f19134 100755 --- a/sdk/src/main/java/org/zstack/sdk/ZSClient.java +++ b/sdk/src/main/java/org/zstack/sdk/ZSClient.java @@ -121,6 +121,10 @@ public ErrorCode deserialize(JsonElement jsonElement, Type type, JsonDeserializa if (item != null && item.isJsonPrimitive()) { wrapper.setGlobalErrorCode(item.getAsString()); } + item = object.get("message"); + if (item != null && item.isJsonPrimitive()) { + wrapper.setMessage(item.getAsString()); + } return wrapper; } } diff --git a/sdk/src/main/java/org/zstack/sdk/iam2/container/SetIAM2ProjectContainerClusterAction.java b/sdk/src/main/java/org/zstack/sdk/iam2/container/SetIAM2ProjectContainerClusterAction.java index 0abbe00ba87..8ee944d7ba8 100644 --- a/sdk/src/main/java/org/zstack/sdk/iam2/container/SetIAM2ProjectContainerClusterAction.java +++ b/sdk/src/main/java/org/zstack/sdk/iam2/container/SetIAM2ProjectContainerClusterAction.java @@ -34,6 +34,9 @@ public Result throwExceptionIfError() { @Param(required = true, nonempty = false, nullElements = false, emptyString = true, noTrim = false) public java.lang.Long clusterId; + @Param(required = false, nonempty = false, nullElements = false, emptyString = true, noTrim = false) + public java.lang.Boolean force; + @Param(required = false) public java.util.List systemTags; diff --git a/sdk/src/main/java/org/zstack/sdk/zwatch/resnotify/DeleteResNotifySubscriptionAction.java b/sdk/src/main/java/org/zstack/sdk/zwatch/resnotify/DeleteResNotifySubscriptionAction.java new file mode 100644 index 00000000000..9266477e583 --- /dev/null +++ b/sdk/src/main/java/org/zstack/sdk/zwatch/resnotify/DeleteResNotifySubscriptionAction.java @@ -0,0 +1,104 @@ +package org.zstack.sdk.zwatch.resnotify; + +import java.util.HashMap; +import java.util.Map; +import org.zstack.sdk.*; + +public class DeleteResNotifySubscriptionAction 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.zwatch.resnotify.DeleteResNotifySubscriptionResult 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 = false, nullElements = false, emptyString = true, noTrim = false) + public java.lang.String uuid; + + @Param(required = false) + public java.lang.String deleteMode = "Permissive"; + + @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.zwatch.resnotify.DeleteResNotifySubscriptionResult value = res.getResult(org.zstack.sdk.zwatch.resnotify.DeleteResNotifySubscriptionResult.class); + ret.value = value == null ? new org.zstack.sdk.zwatch.resnotify.DeleteResNotifySubscriptionResult() : 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 = "DELETE"; + info.path = "/zwatch/resnotify/subscriptions/{uuid}"; + info.needSession = true; + info.needPoll = true; + info.parameterName = ""; + return info; + } + +} diff --git a/sdk/src/main/java/org/zstack/sdk/zwatch/resnotify/DeleteResNotifySubscriptionResult.java b/sdk/src/main/java/org/zstack/sdk/zwatch/resnotify/DeleteResNotifySubscriptionResult.java new file mode 100644 index 00000000000..01e30f49416 --- /dev/null +++ b/sdk/src/main/java/org/zstack/sdk/zwatch/resnotify/DeleteResNotifySubscriptionResult.java @@ -0,0 +1,7 @@ +package org.zstack.sdk.zwatch.resnotify; + + + +public class DeleteResNotifySubscriptionResult { + +} diff --git a/sdk/src/main/java/org/zstack/sdk/zwatch/resnotify/QueryResNotifySubscriptionAction.java b/sdk/src/main/java/org/zstack/sdk/zwatch/resnotify/QueryResNotifySubscriptionAction.java new file mode 100644 index 00000000000..5f4451557aa --- /dev/null +++ b/sdk/src/main/java/org/zstack/sdk/zwatch/resnotify/QueryResNotifySubscriptionAction.java @@ -0,0 +1,75 @@ +package org.zstack.sdk.zwatch.resnotify; + +import java.util.HashMap; +import java.util.Map; +import org.zstack.sdk.*; + +public class QueryResNotifySubscriptionAction extends QueryAction { + + 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.zwatch.resnotify.QueryResNotifySubscriptionResult 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; + } + } + + + + private Result makeResult(ApiResult res) { + Result ret = new Result(); + if (res.error != null) { + ret.error = res.error; + return ret; + } + + org.zstack.sdk.zwatch.resnotify.QueryResNotifySubscriptionResult value = res.getResult(org.zstack.sdk.zwatch.resnotify.QueryResNotifySubscriptionResult.class); + ret.value = value == null ? new org.zstack.sdk.zwatch.resnotify.QueryResNotifySubscriptionResult() : 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 = "GET"; + info.path = "/zwatch/resnotify/subscriptions"; + info.needSession = true; + info.needPoll = false; + info.parameterName = ""; + return info; + } + +} diff --git a/sdk/src/main/java/org/zstack/sdk/zwatch/resnotify/QueryResNotifySubscriptionResult.java b/sdk/src/main/java/org/zstack/sdk/zwatch/resnotify/QueryResNotifySubscriptionResult.java new file mode 100644 index 00000000000..93cd0d2caeb --- /dev/null +++ b/sdk/src/main/java/org/zstack/sdk/zwatch/resnotify/QueryResNotifySubscriptionResult.java @@ -0,0 +1,22 @@ +package org.zstack.sdk.zwatch.resnotify; + + + +public class QueryResNotifySubscriptionResult { + public java.util.List inventories; + public void setInventories(java.util.List inventories) { + this.inventories = inventories; + } + public java.util.List getInventories() { + return this.inventories; + } + + public java.lang.Long total; + public void setTotal(java.lang.Long total) { + this.total = total; + } + public java.lang.Long getTotal() { + return this.total; + } + +} diff --git a/sdk/src/main/java/org/zstack/sdk/zwatch/resnotify/ResNotifySubscriptionInventory.java b/sdk/src/main/java/org/zstack/sdk/zwatch/resnotify/ResNotifySubscriptionInventory.java new file mode 100644 index 00000000000..550d258854b --- /dev/null +++ b/sdk/src/main/java/org/zstack/sdk/zwatch/resnotify/ResNotifySubscriptionInventory.java @@ -0,0 +1,97 @@ +package org.zstack.sdk.zwatch.resnotify; + +import org.zstack.sdk.zwatch.resnotify.ResNotifyType; +import org.zstack.sdk.zwatch.resnotify.ResNotifySubscriptionState; +import org.zstack.sdk.zwatch.resnotify.ResNotifyWebhookRefInventory; + +public class ResNotifySubscriptionInventory { + + public java.lang.String uuid; + public void setUuid(java.lang.String uuid) { + this.uuid = uuid; + } + public java.lang.String getUuid() { + return this.uuid; + } + + public java.lang.String name; + public void setName(java.lang.String name) { + this.name = name; + } + public java.lang.String getName() { + return this.name; + } + + public java.lang.String description; + public void setDescription(java.lang.String description) { + this.description = description; + } + public java.lang.String getDescription() { + return this.description; + } + + public java.lang.String resourceTypes; + public void setResourceTypes(java.lang.String resourceTypes) { + this.resourceTypes = resourceTypes; + } + public java.lang.String getResourceTypes() { + return this.resourceTypes; + } + + public java.lang.String eventTypes; + public void setEventTypes(java.lang.String eventTypes) { + this.eventTypes = eventTypes; + } + public java.lang.String getEventTypes() { + return this.eventTypes; + } + + public ResNotifyType type; + public void setType(ResNotifyType type) { + this.type = type; + } + public ResNotifyType getType() { + return this.type; + } + + public ResNotifySubscriptionState state; + public void setState(ResNotifySubscriptionState state) { + this.state = state; + } + public ResNotifySubscriptionState getState() { + return this.state; + } + + public java.lang.String accountUuid; + public void setAccountUuid(java.lang.String accountUuid) { + this.accountUuid = accountUuid; + } + public java.lang.String getAccountUuid() { + return this.accountUuid; + } + + public java.sql.Timestamp createDate; + public void setCreateDate(java.sql.Timestamp createDate) { + this.createDate = createDate; + } + public java.sql.Timestamp getCreateDate() { + return this.createDate; + } + + public java.sql.Timestamp lastOpDate; + public void setLastOpDate(java.sql.Timestamp lastOpDate) { + this.lastOpDate = lastOpDate; + } + public java.sql.Timestamp getLastOpDate() { + return this.lastOpDate; + } + + public ResNotifyWebhookRefInventory webhookRef; + public void setWebhookRef(ResNotifyWebhookRefInventory webhookRef) { + this.webhookRef = webhookRef; + } + public ResNotifyWebhookRefInventory getWebhookRef() { + return this.webhookRef; + } + +} diff --git a/sdk/src/main/java/org/zstack/sdk/zwatch/resnotify/ResNotifySubscriptionState.java b/sdk/src/main/java/org/zstack/sdk/zwatch/resnotify/ResNotifySubscriptionState.java new file mode 100644 index 00000000000..18351896246 --- /dev/null +++ b/sdk/src/main/java/org/zstack/sdk/zwatch/resnotify/ResNotifySubscriptionState.java @@ -0,0 +1,6 @@ +package org.zstack.sdk.zwatch.resnotify; + +public enum ResNotifySubscriptionState { + Enabled, + Disabled, +} diff --git a/sdk/src/main/java/org/zstack/sdk/zwatch/resnotify/ResNotifyType.java b/sdk/src/main/java/org/zstack/sdk/zwatch/resnotify/ResNotifyType.java new file mode 100644 index 00000000000..5a00aa5b80e --- /dev/null +++ b/sdk/src/main/java/org/zstack/sdk/zwatch/resnotify/ResNotifyType.java @@ -0,0 +1,6 @@ +package org.zstack.sdk.zwatch.resnotify; + +public enum ResNotifyType { + WEBHOOK, + WEBSOCKET, +} diff --git a/sdk/src/main/java/org/zstack/sdk/zwatch/resnotify/ResNotifyWebhookRefInventory.java b/sdk/src/main/java/org/zstack/sdk/zwatch/resnotify/ResNotifyWebhookRefInventory.java new file mode 100644 index 00000000000..cf2c73bc674 --- /dev/null +++ b/sdk/src/main/java/org/zstack/sdk/zwatch/resnotify/ResNotifyWebhookRefInventory.java @@ -0,0 +1,31 @@ +package org.zstack.sdk.zwatch.resnotify; + + + +public class ResNotifyWebhookRefInventory { + + public java.lang.String uuid; + public void setUuid(java.lang.String uuid) { + this.uuid = uuid; + } + public java.lang.String getUuid() { + return this.uuid; + } + + public java.lang.String webhookUrl; + public void setWebhookUrl(java.lang.String webhookUrl) { + this.webhookUrl = webhookUrl; + } + public java.lang.String getWebhookUrl() { + return this.webhookUrl; + } + + public java.lang.String customHeaders; + public void setCustomHeaders(java.lang.String customHeaders) { + this.customHeaders = customHeaders; + } + public java.lang.String getCustomHeaders() { + return this.customHeaders; + } + +} diff --git a/sdk/src/main/java/org/zstack/sdk/zwatch/resnotify/SubscribeResNotifyAction.java b/sdk/src/main/java/org/zstack/sdk/zwatch/resnotify/SubscribeResNotifyAction.java new file mode 100644 index 00000000000..05e5af5d9de --- /dev/null +++ b/sdk/src/main/java/org/zstack/sdk/zwatch/resnotify/SubscribeResNotifyAction.java @@ -0,0 +1,128 @@ +package org.zstack.sdk.zwatch.resnotify; + +import java.util.HashMap; +import java.util.Map; +import org.zstack.sdk.*; + +public class SubscribeResNotifyAction 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.zwatch.resnotify.SubscribeResNotifyResult 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 = false, maxLength = 255, nonempty = false, nullElements = false, emptyString = true, noTrim = false) + public java.lang.String name; + + @Param(required = false, maxLength = 1024, nonempty = false, nullElements = false, emptyString = true, noTrim = false) + public java.lang.String description; + + @Param(required = false, nonempty = false, nullElements = false, emptyString = true, noTrim = false) + public java.util.List resourceTypes; + + @Param(required = false, nonempty = false, nullElements = false, emptyString = true, noTrim = false) + public java.util.List eventTypes; + + @Param(required = false, validValues = {"WEBHOOK"}, nonempty = false, nullElements = false, emptyString = true, noTrim = false) + public java.lang.String type = "WEBHOOK"; + + @Param(required = true, maxLength = 2048, nonempty = false, nullElements = false, emptyString = true, noTrim = false) + public java.lang.String webhookUrl; + + @Param(required = false, maxLength = 256, nonempty = false, nullElements = false, emptyString = true, noTrim = false) + public java.lang.String secret; + + @Param(required = false, maxLength = 2048, nonempty = false, nullElements = false, emptyString = true, noTrim = false) + public java.lang.String customHeaders; + + @Param(required = false) + public java.lang.String resourceUuid; + + @Param(required = false, nonempty = false, nullElements = false, emptyString = true, noTrim = false) + public java.util.List tagUuids; + + @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.zwatch.resnotify.SubscribeResNotifyResult value = res.getResult(org.zstack.sdk.zwatch.resnotify.SubscribeResNotifyResult.class); + ret.value = value == null ? new org.zstack.sdk.zwatch.resnotify.SubscribeResNotifyResult() : 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 = "/zwatch/resnotify/subscriptions"; + info.needSession = true; + info.needPoll = true; + info.parameterName = "params"; + return info; + } + +} diff --git a/sdk/src/main/java/org/zstack/sdk/zwatch/resnotify/SubscribeResNotifyResult.java b/sdk/src/main/java/org/zstack/sdk/zwatch/resnotify/SubscribeResNotifyResult.java new file mode 100644 index 00000000000..60ba0b47c8e --- /dev/null +++ b/sdk/src/main/java/org/zstack/sdk/zwatch/resnotify/SubscribeResNotifyResult.java @@ -0,0 +1,14 @@ +package org.zstack.sdk.zwatch.resnotify; + +import org.zstack.sdk.zwatch.resnotify.ResNotifySubscriptionInventory; + +public class SubscribeResNotifyResult { + public ResNotifySubscriptionInventory inventory; + public void setInventory(ResNotifySubscriptionInventory inventory) { + this.inventory = inventory; + } + public ResNotifySubscriptionInventory getInventory() { + return this.inventory; + } + +} diff --git a/sdk/src/main/java/org/zstack/sdk/zwatch/resnotify/UpdateResNotifySubscriptionAction.java b/sdk/src/main/java/org/zstack/sdk/zwatch/resnotify/UpdateResNotifySubscriptionAction.java new file mode 100644 index 00000000000..3ec79ae0a35 --- /dev/null +++ b/sdk/src/main/java/org/zstack/sdk/zwatch/resnotify/UpdateResNotifySubscriptionAction.java @@ -0,0 +1,125 @@ +package org.zstack.sdk.zwatch.resnotify; + +import java.util.HashMap; +import java.util.Map; +import org.zstack.sdk.*; + +public class UpdateResNotifySubscriptionAction 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.zwatch.resnotify.UpdateResNotifySubscriptionResult 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 = false, nullElements = false, emptyString = true, noTrim = false) + public java.lang.String uuid; + + @Param(required = false, maxLength = 255, nonempty = false, nullElements = false, emptyString = true, noTrim = false) + public java.lang.String name; + + @Param(required = false, maxLength = 1024, nonempty = false, nullElements = false, emptyString = true, noTrim = false) + public java.lang.String description; + + @Param(required = false, nonempty = false, nullElements = false, emptyString = true, noTrim = false) + public java.util.List resourceTypes; + + @Param(required = false, nonempty = false, nullElements = false, emptyString = true, noTrim = false) + public java.util.List eventTypes; + + @Param(required = false, validValues = {"Enabled","Disabled"}, nonempty = false, nullElements = false, emptyString = true, noTrim = false) + public java.lang.String state; + + @Param(required = false, maxLength = 2048, nonempty = false, nullElements = false, emptyString = true, noTrim = false) + public java.lang.String webhookUrl; + + @Param(required = false, maxLength = 256, nonempty = false, nullElements = false, emptyString = true, noTrim = false) + public java.lang.String secret; + + @Param(required = false, maxLength = 2048, nonempty = false, nullElements = false, emptyString = true, noTrim = false) + public java.lang.String customHeaders; + + @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.zwatch.resnotify.UpdateResNotifySubscriptionResult value = res.getResult(org.zstack.sdk.zwatch.resnotify.UpdateResNotifySubscriptionResult.class); + ret.value = value == null ? new org.zstack.sdk.zwatch.resnotify.UpdateResNotifySubscriptionResult() : 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 = "PUT"; + info.path = "/zwatch/resnotify/subscriptions/{uuid}/actions"; + info.needSession = true; + info.needPoll = true; + info.parameterName = "updateResNotifySubscription"; + return info; + } + +} diff --git a/sdk/src/main/java/org/zstack/sdk/zwatch/resnotify/UpdateResNotifySubscriptionResult.java b/sdk/src/main/java/org/zstack/sdk/zwatch/resnotify/UpdateResNotifySubscriptionResult.java new file mode 100644 index 00000000000..8f5b2f62157 --- /dev/null +++ b/sdk/src/main/java/org/zstack/sdk/zwatch/resnotify/UpdateResNotifySubscriptionResult.java @@ -0,0 +1,14 @@ +package org.zstack.sdk.zwatch.resnotify; + +import org.zstack.sdk.zwatch.resnotify.ResNotifySubscriptionInventory; + +public class UpdateResNotifySubscriptionResult { + public ResNotifySubscriptionInventory inventory; + public void setInventory(ResNotifySubscriptionInventory inventory) { + this.inventory = inventory; + } + public ResNotifySubscriptionInventory getInventory() { + return this.inventory; + } + +} diff --git a/storage/src/main/java/org/zstack/storage/primary/ImageCacheCleaner.java b/storage/src/main/java/org/zstack/storage/primary/ImageCacheCleaner.java index 8aab976bf7c..b6db3d7888e 100755 --- a/storage/src/main/java/org/zstack/storage/primary/ImageCacheCleaner.java +++ b/storage/src/main/java/org/zstack/storage/primary/ImageCacheCleaner.java @@ -387,8 +387,8 @@ private List queryCacheOfExpungedImage(String psUuid) { @Transactional protected List createShadowImageCacheVOsForNewDeletedAndOld(String psUuid, ImageCacheCleanParam param) { - // 1. image has been deleted - List staleImageCacheIds = getStaleImageCacheIds(psUuid, false); + // 1. image has been deleted or force cleanup includes images still in ready state with no VMs using them + List staleImageCacheIds = getStaleImageCacheIds(psUuid, param.includeReadyImage); if (staleImageCacheIds == null || staleImageCacheIds.isEmpty()) { return null; } diff --git a/test/pom.xml b/test/pom.xml index 7dfbb0bbcac..6505ec9324c 100644 --- a/test/pom.xml +++ b/test/pom.xml @@ -9,7 +9,7 @@ zstack org.zstack - 5.5.0 + 5.5.0 .. test @@ -292,6 +292,12 @@ org.jasig.cas.client cas-client-core + + org.zstack + eip + 5.5.0 + test + diff --git a/test/src/test/groovy/org/zstack/test/integration/configuration/systemTag/TagPatternResourceTypeCase.groovy b/test/src/test/groovy/org/zstack/test/integration/configuration/systemTag/TagPatternResourceTypeCase.groovy new file mode 100644 index 00000000000..337ba29dfcd --- /dev/null +++ b/test/src/test/groovy/org/zstack/test/integration/configuration/systemTag/TagPatternResourceTypeCase.groovy @@ -0,0 +1,211 @@ +package org.zstack.test.integration.configuration.systemTag + +import org.zstack.core.Platform +import org.zstack.core.db.DatabaseFacade +import org.zstack.core.db.SQL +import org.zstack.header.identity.AccountConstant +import org.zstack.header.tag.TagPatternType +import org.zstack.header.tag.TagPatternVO +import org.zstack.testlib.EnvSpec +import org.zstack.testlib.SubCase + +/** + * ZSTAC-74908: TagPatternVO.resourceType scoping + * + * Verifies: + * 1. AI model tags (resourceType = "ModelVO") are not visible when + * querying tag patterns for other resource types (e.g. VmInstanceVO). + * 2. Universal tags (resourceType = null) remain visible for all + * resource types — backward compatible with pre-upgrade data. + * 3. Upgraded old AI tags get backfilled with resourceType = "ModelVO" + * on next prepareDbInitialValue() run. + */ +class TagPatternResourceTypeCase extends SubCase { + EnvSpec env + DatabaseFacade dbf + + @Override + void setup() { + } + + @Override + void environment() { + env = env {} + } + + @Override + void test() { + env.create { + dbf = bean(DatabaseFacade.class) + testUniversalTagPatternVisibleForAllResourceTypes() + testScopedTagPatternOnlyVisibleForMatchingResourceType() + testQueryFilterByResourceType() + } + } + + /** + * resourceType = null means the tag pattern is universal. + * It should be returned regardless of what resource type is being queried. + */ + void testUniversalTagPatternVisibleForAllResourceTypes() { + // Create a universal tag pattern (simulating pre-upgrade tag) + TagPatternVO universal = new TagPatternVO() + universal.setUuid(Platform.getUuid()) + universal.setName("Priority::High") + universal.setValue("Priority::High") + universal.setColor("red") + universal.setType(TagPatternType.simple) + universal.setResourceType(null) // null = universal + universal.setAccountUuid(AccountConstant.INITIAL_SYSTEM_ADMIN_UUID) + dbf.persist(universal) + + // Verify it can be found without any resourceType filter + TagPatternVO found = dbf.findByUuid(universal.getUuid(), TagPatternVO.class) + assert found != null + assert found.getResourceType() == null + + // Verify it appears in queries for any resource type + // Simulating the filter: resourceType IS NULL OR resourceType = 'ZoneVO' + List results = SQL.New( + "select tp from TagPatternVO tp where tp.uuid = :uuid and tp.resourceType is null", + TagPatternVO.class + ).param("uuid", universal.getUuid()).list() + assert results.size() == 1 + + // Clean up + dbf.removeByPrimaryKey(universal.getUuid(), TagPatternVO.class) + } + + /** + * resourceType = "ModelVO" means the tag pattern is scoped to AI models. + * It should NOT appear when filtering for other resource types. + */ + void testScopedTagPatternOnlyVisibleForMatchingResourceType() { + // Create an AI-scoped tag pattern + TagPatternVO aiTag = new TagPatternVO() + aiTag.setUuid(Platform.getUuid()) + aiTag.setName("AI::LLM") + aiTag.setValue("AI::LLM") + aiTag.setColor("blue") + aiTag.setType(TagPatternType.simple) + aiTag.setResourceType("ModelVO") + aiTag.setAccountUuid(AccountConstant.INITIAL_SYSTEM_ADMIN_UUID) + dbf.persist(aiTag) + + TagPatternVO found = dbf.findByUuid(aiTag.getUuid(), TagPatternVO.class) + assert found != null + assert found.getResourceType() == "ModelVO" + + // Should be found when filtering for ModelVO + List modelResults = SQL.New( + "select tp from TagPatternVO tp where tp.uuid = :uuid and tp.resourceType = :resType", + TagPatternVO.class + ).param("uuid", aiTag.getUuid()).param("resType", "ModelVO").list() + assert modelResults.size() == 1 + + // Should NOT be found when filtering for VmInstanceVO + List vmResults = SQL.New( + "select tp from TagPatternVO tp where tp.uuid = :uuid and tp.resourceType = :resType", + TagPatternVO.class + ).param("uuid", aiTag.getUuid()).param("resType", "VmInstanceVO").list() + assert vmResults.size() == 0 + + // Clean up + dbf.removeByPrimaryKey(aiTag.getUuid(), TagPatternVO.class) + } + + /** + * Test the combined query pattern that the UI should use: + * WHERE resourceType IS NULL OR resourceType = :targetResourceType + * + * This ensures: + * - Universal tags (null) are always included + * - Scoped tags only appear for matching resource types + * - AI tags do not leak into VM/Zone/etc pages + */ + void testQueryFilterByResourceType() { + // Create a universal tag + TagPatternVO universal = new TagPatternVO() + universal.setUuid(Platform.getUuid()) + universal.setName("Env::Production") + universal.setValue("Env::Production") + universal.setColor("green") + universal.setType(TagPatternType.simple) + universal.setResourceType(null) + universal.setAccountUuid(AccountConstant.INITIAL_SYSTEM_ADMIN_UUID) + dbf.persist(universal) + + // Create an AI-scoped tag + TagPatternVO aiTag = new TagPatternVO() + aiTag.setUuid(Platform.getUuid()) + aiTag.setName("AI::Rerank") + aiTag.setValue("AI::Rerank") + aiTag.setColor("purple") + aiTag.setType(TagPatternType.simple) + aiTag.setResourceType("ModelVO") + aiTag.setAccountUuid(AccountConstant.INITIAL_SYSTEM_ADMIN_UUID) + dbf.persist(aiTag) + + // Create a VM-scoped tag + TagPatternVO vmTag = new TagPatternVO() + vmTag.setUuid(Platform.getUuid()) + vmTag.setName("VM::HighPerf") + vmTag.setValue("VM::HighPerf") + vmTag.setColor("orange") + vmTag.setType(TagPatternType.simple) + vmTag.setResourceType("VmInstanceVO") + vmTag.setAccountUuid(AccountConstant.INITIAL_SYSTEM_ADMIN_UUID) + dbf.persist(vmTag) + + // Query for VmInstanceVO page: should see universal + VM tag, NOT AI tag + List vmPageTags = SQL.New( + "select tp from TagPatternVO tp" + + " where tp.uuid in (:uuids)" + + " and (tp.resourceType is null or tp.resourceType = :resType)", + TagPatternVO.class + ).param("uuids", [universal.getUuid(), aiTag.getUuid(), vmTag.getUuid()]) + .param("resType", "VmInstanceVO") + .list() + + assert vmPageTags.size() == 2 + def vmPageUuids = vmPageTags.collect { it.getUuid() } as Set + assert vmPageUuids.contains(universal.getUuid()) + assert vmPageUuids.contains(vmTag.getUuid()) + assert !vmPageUuids.contains(aiTag.getUuid()) + + // Query for ModelVO page: should see universal + AI tag, NOT VM tag + List modelPageTags = SQL.New( + "select tp from TagPatternVO tp" + + " where tp.uuid in (:uuids)" + + " and (tp.resourceType is null or tp.resourceType = :resType)", + TagPatternVO.class + ).param("uuids", [universal.getUuid(), aiTag.getUuid(), vmTag.getUuid()]) + .param("resType", "ModelVO") + .list() + + assert modelPageTags.size() == 2 + def modelPageUuids = modelPageTags.collect { it.getUuid() } as Set + assert modelPageUuids.contains(universal.getUuid()) + assert modelPageUuids.contains(aiTag.getUuid()) + assert !modelPageUuids.contains(vmTag.getUuid()) + + // Query with no resource type filter: should see ALL tags + List allTags = SQL.New( + "select tp from TagPatternVO tp where tp.uuid in (:uuids)", + TagPatternVO.class + ).param("uuids", [universal.getUuid(), aiTag.getUuid(), vmTag.getUuid()]) + .list() + + assert allTags.size() == 3 + + // Clean up + [universal, aiTag, vmTag].each { + dbf.removeByPrimaryKey(it.getUuid(), TagPatternVO.class) + } + } + + @Override + void clean() { + env.delete() + } +} diff --git a/test/src/test/groovy/org/zstack/test/integration/core/ErrorCodeI18nCase.groovy b/test/src/test/groovy/org/zstack/test/integration/core/ErrorCodeI18nCase.groovy new file mode 100644 index 00000000000..53c7f67c864 --- /dev/null +++ b/test/src/test/groovy/org/zstack/test/integration/core/ErrorCodeI18nCase.groovy @@ -0,0 +1,182 @@ +package org.zstack.test.integration.core + +import org.zstack.core.errorcode.GlobalErrorCodeI18nServiceImpl +import org.zstack.core.errorcode.LocaleUtils +import org.zstack.header.errorcode.ErrorCode +import org.zstack.header.errorcode.ErrorCodeList +import org.zstack.testlib.SubCase + +class ErrorCodeI18nCase extends SubCase { + + @Override + void setup() { + INCLUDE_CORE_SERVICES = false + } + + @Override + void environment() { + } + + @Override + void test() { + testLocaleUtilsExactMatch() + testLocaleUtilsBaseLanguageFallback() + testLocaleUtilsQValueSorting() + testLocaleUtilsNullAndEmpty() + testLocaleUtilsNoMatch() + testLocaleUtilsCaseInsensitive() + testLocaleUtilsMalformedHeader() + testLocaleUtilsComplexBrowserHeader() + testErrorCodeCopyConstructor() + testErrorCodeCopyConstructorWithNulls() + testMessageGuaranteeFallbackToDetails() + testMessageGuaranteeFallbackToDescription() + testMessageGuaranteeOnCauseChain() + } + + @Override + void clean() { + } + + // ---- LocaleUtils ---- + + void testLocaleUtilsExactMatch() { + def available = ["zh_CN", "en_US"] as Set + assert LocaleUtils.resolveLocale("zh-CN", available) == "zh_CN" + assert LocaleUtils.resolveLocale("en-US", available) == "en_US" + } + + void testLocaleUtilsBaseLanguageFallback() { + def available = ["zh_CN", "en_US"] as Set + assert LocaleUtils.resolveLocale("en", available) == "en_US" + assert LocaleUtils.resolveLocale("zh", available) == "zh_CN" + } + + void testLocaleUtilsQValueSorting() { + def available = ["zh_CN", "en_US"] as Set + assert LocaleUtils.resolveLocale("zh-CN,en;q=0.8", available) == "zh_CN" + assert LocaleUtils.resolveLocale("en-US,zh-CN;q=0.5", available) == "en_US" + // q-value should override header order + assert LocaleUtils.resolveLocale("en;q=0.8,zh-CN;q=1.0", available) == "zh_CN" + } + + void testLocaleUtilsNullAndEmpty() { + def available = ["zh_CN", "en_US"] as Set + assert LocaleUtils.resolveLocale(null, available) == "en_US" + assert LocaleUtils.resolveLocale("", available) == "en_US" + assert LocaleUtils.resolveLocale(" ", available) == "en_US" + } + + void testLocaleUtilsNoMatch() { + def available = ["zh_CN", "en_US"] as Set + assert LocaleUtils.resolveLocale("ja-JP,ko-KR", available) == "en_US" + } + + void testLocaleUtilsCaseInsensitive() { + def available = ["zh_CN", "en_US"] as Set + assert LocaleUtils.resolveLocale("ZH-CN", available) == "zh_CN" + assert LocaleUtils.resolveLocale("EN-US", available) == "en_US" + } + + void testLocaleUtilsMalformedHeader() { + def available = ["zh_CN", "en_US"] as Set + // malformed header should fall back to default + assert LocaleUtils.resolveLocale(";;;,,,", available) == "en_US" + } + + void testLocaleUtilsComplexBrowserHeader() { + def available = ["zh_CN", "en_US"] as Set + // real Chrome header: zh-CN,zh;q=0.9,en;q=0.8,en-GB;q=0.7,en-US;q=0.6 + assert LocaleUtils.resolveLocale("zh-CN,zh;q=0.9,en;q=0.8,en-GB;q=0.7,en-US;q=0.6", available) == "zh_CN" + // q=0 means "not acceptable" — should be skipped + assert LocaleUtils.resolveLocale("zh-CN;q=0,en-US;q=1.0", available) == "en_US" + } + + // ---- ErrorCode copy constructor ---- + + void testErrorCodeCopyConstructor() { + def original = new ErrorCode("SYS.1000", "System Error", "something failed") + original.setElaboration("elaboration text") + original.setLocation("org.zstack.Foo:123") + original.setCost("50ms") + original.setGlobalErrorCode("ORG_ZSTACK_FOO_10000") + original.setMessage("系统错误") + original.setFormatArgs(["arg1", "arg2"] as String[]) + + def opaque = new LinkedHashMap() + opaque.put("key1", "value1") + original.setOpaque(opaque) + + def cause = new ErrorCode("INTERNAL.1001", "Internal Error") + original.setCause(cause) + + def copy = new ErrorCode(original) + + assert copy.code == original.code + assert copy.description == original.description + assert copy.details == original.details + assert copy.elaboration == original.elaboration + assert copy.location == original.location + assert copy.cost == original.cost + assert copy.globalErrorCode == original.globalErrorCode + assert copy.message == original.message + assert copy.opaque.is(original.opaque) + assert copy.cause.is(original.cause) + // formatArgs should be cloned, not shared + assert copy.formatArgs == original.formatArgs + assert !copy.formatArgs.is(original.formatArgs) + } + + void testErrorCodeCopyConstructorWithNulls() { + def original = new ErrorCode("SYS.1000", "System Error") + def copy = new ErrorCode(original) + + assert copy.code == original.code + assert copy.description == original.description + assert copy.details == null + assert copy.cost == null + assert copy.opaque == null + assert copy.message == null + assert copy.globalErrorCode == null + assert copy.formatArgs == null + } + + // ---- message guarantee (localizeErrorCode always populates message) ---- + + void testMessageGuaranteeFallbackToDetails() { + def i18n = new GlobalErrorCodeI18nServiceImpl() + // no i18n JSON loaded, so getLocalizedMessage returns null + // localizeErrorCode should fall back to details + def error = new ErrorCode("SYS.1000", "System Error", "disk full on /dev/sda1") + assert error.getMessage() == null + + i18n.localizeErrorCode(error, "en_US") + + assert error.getMessage() == "disk full on /dev/sda1" + } + + void testMessageGuaranteeFallbackToDescription() { + def i18n = new GlobalErrorCodeI18nServiceImpl() + // no details, should fall back to description + def error = new ErrorCode("SYS.1000", "System Error") + assert error.getMessage() == null + + i18n.localizeErrorCode(error, "en_US") + + assert error.getMessage() == "System Error" + } + + void testMessageGuaranteeOnCauseChain() { + def i18n = new GlobalErrorCodeI18nServiceImpl() + def root = new ErrorCode("INTERNAL.1001", "Internal Error", "root cause detail") + def mid = new ErrorCode("SYS.1000", "System Error") + mid.setCause(root) + + i18n.localizeErrorCode(mid, "en_US") + + // both should have message populated + assert mid.getMessage() == "System Error" + assert root.getMessage() == "root cause detail" + } + +} diff --git a/test/src/test/groovy/org/zstack/test/integration/core/taskqueue/CoalesceQueueCase.groovy b/test/src/test/groovy/org/zstack/test/integration/core/taskqueue/CoalesceQueueCase.groovy new file mode 100644 index 00000000000..e81eb8b19b4 --- /dev/null +++ b/test/src/test/groovy/org/zstack/test/integration/core/taskqueue/CoalesceQueueCase.groovy @@ -0,0 +1,740 @@ +package org.zstack.test.integration.core.chaintask + +import org.zstack.core.thread.CoalesceQueue +import org.zstack.core.thread.ReturnValueCoalesceQueue +import org.zstack.header.core.Completion +import org.zstack.header.core.ReturnValueCompletion +import org.zstack.header.errorcode.ErrorCode +import org.zstack.testlib.core.FailCoalesceQueue +import org.zstack.testlib.core.ThrowOnSuccessCompletion +import org.zstack.testlib.core.ThrowOnFailCompletion +import org.zstack.testlib.core.FailReturnValueCoalesceQueue +import org.zstack.testlib.core.ThrowOnSuccessReturnValueCompletion +import org.zstack.testlib.core.ThrowOnFailReturnValueCompletion +import org.zstack.testlib.SubCase + +import java.util.concurrent.CountDownLatch +import java.util.concurrent.TimeUnit +import java.util.concurrent.atomic.AtomicInteger + +class CoalesceQueueCase extends SubCase { + @Override + void clean() { + } + + @Override + void setup() { + } + + @Override + void environment() { + } + + @Override + void test() { + testCoalesceMultipleRequests() + testDifferentSignaturesNotCoalesced() + testBatchFailureNotifiesAllRequests() + testBatchThrowExceptionNotifiesAllRequests() + testReturnValueCompletion() + testResultCalculationFailure() + testSequentialBatches() + testHighVolumeNoLossAcrossBatches() + testCompletionSuccessThrowDoesNotBlockChain() + testCompletionFailThrowDoesNotBlockChain() + testRvExecuteBatchThrowDoesNotBlockChain() + testRvCompletionSuccessThrowDoesNotBlockChain() + testRvCompletionFailThrowDoesNotBlockChain() + testCalculateResultFailDoesNotBlockChain() + } + + void testCoalesceMultipleRequests() { + def requestCount = 10 + def completionLatch = new CountDownLatch(requestCount) + def batchExecutionCount = new AtomicInteger(0) + def processedItems = Collections.synchronizedList(new ArrayList()) + def completedTokens = Collections.synchronizedSet(new LinkedHashSet()) + + def queue = new CoalesceQueue() { + @Override + protected String getName() { + return "test-coalesce" + } + + @Override + protected void executeBatch(List items, Completion completion) { + batchExecutionCount.incrementAndGet() + processedItems.addAll(items) + + new Thread({ + try { + TimeUnit.MILLISECONDS.sleep(100) + } catch (InterruptedException ignored) { + } + completion.success() + }).start() + } + } + + def signature = "host-1" + (0.. + def token = "done-${idx}" + queue.submit(signature, idx, new Completion(null) { + @Override + void success() { + completedTokens.add(token) + completionLatch.countDown() + } + + @Override + void fail(ErrorCode errorCode) { + completedTokens.add(token) + completionLatch.countDown() + } + }) + } + + assert completionLatch.await(10, TimeUnit.SECONDS) + assert processedItems.size() == requestCount + assert batchExecutionCount.get() < requestCount + assert completedTokens.size() == requestCount + (0.. + assert completedTokens.contains("done-${idx}") + } + } + + void testDifferentSignaturesNotCoalesced() { + def signaturesCount = 3 + def requestsPerSignature = 5 + def totalRequests = signaturesCount * requestsPerSignature + def completionLatch = new CountDownLatch(totalRequests) + def batchExecutionCount = new AtomicInteger(0) + def completedTokens = Collections.synchronizedSet(new LinkedHashSet()) + + def queue = new CoalesceQueue() { + @Override + protected String getName() { + return "test-multi-sig" + } + + @Override + protected void executeBatch(List items, Completion completion) { + batchExecutionCount.incrementAndGet() + completion.success() + } + } + + (0.. + def signature = "host-${sig}" + (0.. + def item = "${signature}-item-${idx}" + def token = "done-${item}" + queue.submit(signature, item, new Completion(null) { + @Override + void success() { + completedTokens.add(token) + completionLatch.countDown() + } + + @Override + void fail(ErrorCode errorCode) { + completedTokens.add(token) + completionLatch.countDown() + } + }) + } + } + + assert completionLatch.await(10, TimeUnit.SECONDS) + assert batchExecutionCount.get() >= signaturesCount + assert completedTokens.size() == totalRequests + (0.. + def signature = "host-${sig}" + (0.. + assert completedTokens.contains("done-${signature}-item-${idx}") + } + } + } + + void testBatchFailureNotifiesAllRequests() { + def requestCount = 5 + def completionLatch = new CountDownLatch(requestCount) + def failureCount = new AtomicInteger(0) + def testError = org.zstack.core.Platform.operr(org.zstack.utils.clouderrorcode.CloudOperationsErrorCode.ORG_ZSTACK_CORE_THREAD_10004, "test error") + def completedTokens = Collections.synchronizedSet(new LinkedHashSet()) + + def queue = new CoalesceQueue() { + @Override + protected String getName() { + return "test-failure" + } + + @Override + protected void executeBatch(List items, Completion completion) { + completion.fail(testError) + } + } + + def signature = "host-fail" + (0.. + def token = "fail-${idx}" + queue.submit(signature, idx, new Completion(null) { + @Override + void success() { + completedTokens.add(token) + completionLatch.countDown() + } + + @Override + void fail(ErrorCode errorCode) { + failureCount.incrementAndGet() + completedTokens.add(token) + completionLatch.countDown() + } + }) + } + + assert completionLatch.await(10, TimeUnit.SECONDS) + assert failureCount.get() == requestCount + assert completedTokens.size() == requestCount + (0.. + assert completedTokens.contains("fail-${idx}") + } + } + + void testBatchThrowExceptionNotifiesAllRequests() { + def requestCount = 5 + def completionLatch = new CountDownLatch(requestCount) + def failureCount = new AtomicInteger(0) + def completedTokens = Collections.synchronizedSet(new LinkedHashSet()) + + def queue = new FailCoalesceQueue() + + def signature = "host-throw" + (0.. + def token = "throw-${idx}" + queue.submit(signature, idx, new Completion(null) { + @Override + void success() { + completedTokens.add(token) + completionLatch.countDown() + } + + @Override + void fail(ErrorCode errorCode) { + failureCount.incrementAndGet() + completedTokens.add(token) + completionLatch.countDown() + } + }) + } + + assert completionLatch.await(10, TimeUnit.SECONDS) + assert failureCount.get() == requestCount + assert completedTokens.size() == requestCount + (0.. + assert completedTokens.contains("throw-${idx}") + } + } + + + void testReturnValueCompletion() { + def requestCount = 5 + def completionLatch = new CountDownLatch(requestCount) + def receivedResults = Collections.synchronizedMap(new LinkedHashMap()) + def mismatches = Collections.synchronizedList(new ArrayList()) + def batchResult = "batch-success" + + def queue = new ReturnValueCoalesceQueue() { + @Override + protected String getName() { + return "test-return-value" + } + + @Override + protected void executeBatch(List items, ReturnValueCompletion completion) { + completion.success(batchResult) + } + + @Override + protected String calculateResult(Integer item, String r) { + return "${r}-item-${item}" + } + } + + def signature = "host-result" + (0.. + queue.submit(signature, idx, new ReturnValueCompletion(null) { + @Override + void success(String result) { + def expected = String.format("%s-item-%s", batchResult, idx) + if (result != expected) { + mismatches.add(String.format("item-%s=%s", idx, result)) + } + receivedResults.put(idx, result) + completionLatch.countDown() + } + + @Override + void fail(ErrorCode errorCode) { + completionLatch.countDown() + } + }) + } + + assert completionLatch.await(10, TimeUnit.SECONDS) + assert receivedResults.size() == requestCount + assert mismatches.isEmpty() + (0.. + def expected = String.format("%s-item-%s", batchResult, idx) + assert receivedResults.get(idx) == expected + } + } + + void testResultCalculationFailure() { + def completionLatch = new CountDownLatch(2) + def successCount = new AtomicInteger(0) + def failCount = new AtomicInteger(0) + + def queue = new ReturnValueCoalesceQueue() { + @Override + protected String getName() { + return "test-calc-fail" + } + + @Override + protected void executeBatch(List items, ReturnValueCompletion completion) { + completion.success(null) + } + + @Override + protected String calculateResult(Integer item, Void batchResult) { + if (item == 0) { + throw new RuntimeException("Calculation failed for item 0 (on purpose)") + } + return "success" + } + } + + def signature = "host-calc" + queue.submit(signature, 0, new ReturnValueCompletion(null) { + @Override + void success(String ret) { + successCount.incrementAndGet() + completionLatch.countDown() + } + + @Override + void fail(ErrorCode errorCode) { + failCount.incrementAndGet() + completionLatch.countDown() + } + }) + + queue.submit(signature, 1, new ReturnValueCompletion(null) { + @Override + void success(String ret) { + successCount.incrementAndGet() + completionLatch.countDown() + } + + @Override + void fail(ErrorCode errorCode) { + failCount.incrementAndGet() + completionLatch.countDown() + } + }) + + assert completionLatch.await(10, TimeUnit.SECONDS) + assert successCount.get() == 1 + assert failCount.get() == 1 + } + + void testSequentialBatches() { + def firstBatchStart = new CountDownLatch(1) + def firstBatchContinue = new CountDownLatch(1) + def secondBatchStart = new CountDownLatch(1) + def secondBatchContinue = new CountDownLatch(1) + def allComplete = new CountDownLatch(6) + def batches = Collections.synchronizedList(new ArrayList>()) + + def queue = new CoalesceQueue() { + @Override + protected String getName() { + return "test-sequential" + } + + @Override + protected void executeBatch(List items, Completion completion) { + batches.add(new ArrayList<>(items)) + + if (batches.size() == 1) { + firstBatchStart.countDown() + try { + firstBatchContinue.await(5, TimeUnit.SECONDS) + } catch (InterruptedException ignored) { + } + } else if (batches.size() == 2) { + secondBatchStart.countDown() + try { + secondBatchContinue.await(5, TimeUnit.SECONDS) + } catch (InterruptedException ignored) { + } + } + + completion.success() + } + } + + def signature = "host-seq" + queue.submit(signature, 0, new Completion(null) { + @Override + void success() { + allComplete.countDown() + } + + @Override + void fail(ErrorCode errorCode) { + allComplete.countDown() + } + }) + + assert firstBatchStart.await(5, TimeUnit.SECONDS) + + (1..<4).each { idx -> + queue.submit(signature, idx, new Completion(null) { + @Override + void success() { + allComplete.countDown() + } + + @Override + void fail(ErrorCode errorCode) { + allComplete.countDown() + } + }) + } + + // release first batch so chain.next() fires and second batch can start + firstBatchContinue.countDown() + assert secondBatchStart.await(5, TimeUnit.SECONDS) + + // submit more items while second batch is blocked on secondBatchContinue + (4..<6).each { idx -> + queue.submit(signature, idx, new Completion(null) { + @Override + void success() { + allComplete.countDown() + } + + @Override + void fail(ErrorCode errorCode) { + allComplete.countDown() + } + }) + } + + secondBatchContinue.countDown() + assert allComplete.await(10, TimeUnit.SECONDS) + assert batches.size() == 3 + assert batches.get(0) == [0] + assert batches.get(1).containsAll([1, 2, 3]) + assert batches.get(2).containsAll([4, 5]) + } + + void testHighVolumeNoLossAcrossBatches() { + def requestCount = 300 + def completionLatch = new CountDownLatch(requestCount) + def processedItems = Collections.synchronizedSet(new LinkedHashSet()) + def batchCount = new AtomicInteger(0) + + def queue = new CoalesceQueue() { + @Override + protected String getName() { + return "test-high-volume" + } + + @Override + protected void executeBatch(List items, Completion completion) { + batchCount.incrementAndGet() + processedItems.addAll(items) + + new Thread({ + try { + TimeUnit.MILLISECONDS.sleep(3) + } catch (InterruptedException ignored) { + } + completion.success() + }).start() + } + } + + def signature = "host-high-volume" + (0.. + queue.submit(signature, idx, new Completion(null) { + @Override + void success() { + completionLatch.countDown() + } + + @Override + void fail(ErrorCode errorCode) { + completionLatch.countDown() + } + }) + } + + assert completionLatch.await(6, TimeUnit.SECONDS) + assert processedItems.size() == requestCount + assert batchCount.get() >= 1 + } + + void testCompletionSuccessThrowDoesNotBlockChain() { + def throwLatch = new CountDownLatch(1) + def normalLatch = new CountDownLatch(1) + + def queue = new CoalesceQueue() { + @Override + protected String getName() { + return "test-success-throw" + } + + @Override + protected void executeBatch(List items, Completion completion) { + completion.success() + } + } + + def signature = "host-throw-success" + + // first request: Java Completion that throws on success() — AJ should catch it + queue.submit(signature, 0, new ThrowOnSuccessCompletion(throwLatch)) + + assert throwLatch.await(5, TimeUnit.SECONDS) + + // second request on same signature: must succeed if chain is not stuck + queue.submit(signature, 1, new Completion(null) { + @Override + void success() { + normalLatch.countDown() + } + + @Override + void fail(ErrorCode errorCode) { + normalLatch.countDown() + } + }) + + assert normalLatch.await(5, TimeUnit.SECONDS) : "chain stuck after completion.success() threw exception" + } + + void testCompletionFailThrowDoesNotBlockChain() { + def throwLatch = new CountDownLatch(1) + def normalLatch = new CountDownLatch(1) + + def queue = new CoalesceQueue() { + @Override + protected String getName() { + return "test-fail-throw" + } + + @Override + protected void executeBatch(List items, Completion completion) { + completion.fail(org.zstack.core.Platform.operr( + org.zstack.utils.clouderrorcode.CloudOperationsErrorCode.ORG_ZSTACK_CORE_THREAD_10004, + "intentional batch failure")) + } + } + + def signature = "host-throw-fail" + + // first request: Java Completion that throws on fail() — AJ should catch it + queue.submit(signature, 0, new ThrowOnFailCompletion(throwLatch)) + + assert throwLatch.await(5, TimeUnit.SECONDS) + + // second request on same signature: must succeed if chain is not stuck + queue.submit(signature, 1, new Completion(null) { + @Override + void success() { + normalLatch.countDown() + } + + @Override + void fail(ErrorCode errorCode) { + normalLatch.countDown() + } + }) + + assert normalLatch.await(5, TimeUnit.SECONDS) : "chain stuck after completion.fail() threw exception" + } + + void testRvExecuteBatchThrowDoesNotBlockChain() { + def throwLatch = new CountDownLatch(1) + def normalLatch = new CountDownLatch(1) + + def queue = new FailReturnValueCoalesceQueue() + + def signature = "host-rv-throw" + queue.submit(signature, 0, new ThrowOnFailReturnValueCompletion(throwLatch)) + + assert throwLatch.await(5, TimeUnit.SECONDS) + + queue.submit(signature, 1, new ReturnValueCompletion(null) { + @Override + void success(String returnValue) { + normalLatch.countDown() + } + + @Override + void fail(ErrorCode errorCode) { + normalLatch.countDown() + } + }) + + assert normalLatch.await(5, TimeUnit.SECONDS) : "chain stuck after RV executeBatch threw exception" + } + + void testRvCompletionSuccessThrowDoesNotBlockChain() { + def throwLatch = new CountDownLatch(1) + def normalLatch = new CountDownLatch(1) + + def queue = new ReturnValueCoalesceQueue() { + @Override + protected String getName() { + return "test-rv-success-throw" + } + + @Override + protected void executeBatch(List items, ReturnValueCompletion completion) { + completion.success("ok") + } + + @Override + protected String calculateResult(Integer item, String batchResult) { + return batchResult + } + } + + def signature = "host-rv-success-throw" + queue.submit(signature, 0, new ThrowOnSuccessReturnValueCompletion(throwLatch)) + + assert throwLatch.await(5, TimeUnit.SECONDS) + + queue.submit(signature, 1, new ReturnValueCompletion(null) { + @Override + void success(String returnValue) { + normalLatch.countDown() + } + + @Override + void fail(ErrorCode errorCode) { + normalLatch.countDown() + } + }) + + assert normalLatch.await(5, TimeUnit.SECONDS) : "chain stuck after RV completion.success() threw exception" + } + + void testRvCompletionFailThrowDoesNotBlockChain() { + def throwLatch = new CountDownLatch(1) + def normalLatch = new CountDownLatch(1) + + def queue = new ReturnValueCoalesceQueue() { + @Override + protected String getName() { + return "test-rv-fail-throw" + } + + @Override + protected void executeBatch(List items, ReturnValueCompletion completion) { + completion.fail(org.zstack.core.Platform.operr( + org.zstack.utils.clouderrorcode.CloudOperationsErrorCode.ORG_ZSTACK_CORE_THREAD_10004, + "intentional rv batch failure")) + } + + @Override + protected String calculateResult(Integer item, String batchResult) { + return null + } + } + + def signature = "host-rv-fail-throw" + queue.submit(signature, 0, new ThrowOnFailReturnValueCompletion(throwLatch)) + + assert throwLatch.await(5, TimeUnit.SECONDS) + + queue.submit(signature, 1, new ReturnValueCompletion(null) { + @Override + void success(String returnValue) { + normalLatch.countDown() + } + + @Override + void fail(ErrorCode errorCode) { + normalLatch.countDown() + } + }) + + assert normalLatch.await(5, TimeUnit.SECONDS) : "chain stuck after RV completion.fail() threw exception" + } + + void testCalculateResultFailDoesNotBlockChain() { + def firstLatch = new CountDownLatch(2) + def normalLatch = new CountDownLatch(1) + + def queue = new ReturnValueCoalesceQueue() { + @Override + protected String getName() { + return "test-calc-fail-chain" + } + + @Override + protected void executeBatch(List items, ReturnValueCompletion completion) { + completion.success("ok") + } + + @Override + protected String calculateResult(Integer item, String batchResult) { + if (item == 0) { + throw new RuntimeException("intentional calculateResult failure") + } + return batchResult + } + } + + def signature = "host-calc-fail-chain" + + // item 0 will throw in calculateResult, item 1 should still succeed + (0..1).each { idx -> + queue.submit(signature, idx, new ReturnValueCompletion(null) { + @Override + void success(String returnValue) { + firstLatch.countDown() + } + + @Override + void fail(ErrorCode errorCode) { + firstLatch.countDown() + } + }) + } + + assert firstLatch.await(5, TimeUnit.SECONDS) + + // subsequent request must work — chain not stuck + queue.submit(signature, 2, new ReturnValueCompletion(null) { + @Override + void success(String returnValue) { + normalLatch.countDown() + } + + @Override + void fail(ErrorCode errorCode) { + normalLatch.countDown() + } + }) + + assert normalLatch.await(5, TimeUnit.SECONDS) : "chain stuck after calculateResult threw exception" + } +} diff --git a/test/src/test/groovy/org/zstack/test/integration/kvm/vm/VmPmuConfigCase.groovy b/test/src/test/groovy/org/zstack/test/integration/kvm/vm/VmPmuConfigCase.groovy new file mode 100644 index 00000000000..5fa2bd94001 --- /dev/null +++ b/test/src/test/groovy/org/zstack/test/integration/kvm/vm/VmPmuConfigCase.groovy @@ -0,0 +1,194 @@ +package org.zstack.test.integration.kvm.vm + +import org.springframework.http.HttpEntity +import org.zstack.compute.vm.VmGlobalConfig +import org.zstack.kvm.KVMAgentCommands +import org.zstack.kvm.KVMConstant +import org.zstack.sdk.GlobalConfigInventory +import org.zstack.sdk.VmInstanceInventory +import org.zstack.test.integration.kvm.KvmTest +import org.zstack.testlib.EnvSpec +import org.zstack.testlib.SubCase +import org.zstack.utils.data.SizeUnit +import org.zstack.utils.gson.JSONObjectUtil + +/** + * Test VM PMU configuration. + * See ZSTAC-76375: Kunpeng-920 7270Z kernel panic due to PMMIR_EL1. + */ +class VmPmuConfigCase extends SubCase { + EnvSpec env + + @Override + void clean() { + env.delete() + } + + @Override + void setup() { + useSpring(KvmTest.springSpec) + } + + @Override + void environment() { + env = env { + instanceOffering { + name = "instanceOffering" + memory = SizeUnit.GIGABYTE.toByte(2) + cpu = 1 + } + + sftpBackupStorage { + name = "sftp" + url = "/sftp" + username = "root" + password = "password" + hostname = "localhost" + + image { + name = "image1" + url = "http://zstack.org/download/test.qcow2" + } + } + + zone { + name = "zone" + description = "test" + + cluster { + name = "cluster" + hypervisorType = "KVM" + + kvm { + name = "kvm" + managementIp = "localhost" + username = "root" + password = "password" + } + + attachPrimaryStorage("local") + attachL2Network("l2") + } + + attachBackupStorage("sftp") + + localPrimaryStorage { + name = "local" + url = "/local_ps" + } + + l2NoVlanNetwork { + name = "l2" + physicalInterface = "eth0" + + l3Network { + name = "l3" + + ip { + startIp = "192.168.100.10" + endIp = "192.168.100.100" + netmask = "255.255.255.0" + gateway = "192.168.100.1" + } + } + } + } + } + } + + @Override + void test() { + env.create() + testPmuGlobalConfigExists() + testPmuDefaultOnX86() + testPmuResourceConfigOverride() + } + + void testPmuGlobalConfigExists() { + def configs = queryGlobalConfig { + conditions = ["category=${VmGlobalConfig.CATEGORY}", "name=${VmGlobalConfig.VM_PMU.name}"] + } + + assert configs.size() == 1 : "vm.pmu GlobalConfig should exist" + def config = configs[0] as GlobalConfigInventory + assert config.defaultValue == "false" : "vm.pmu should default to false" + } + + void testPmuDefaultOnX86() { + def image = env.inventoryByName("image1") + def l3 = env.inventoryByName("l3") + def instance = env.inventoryByName("instanceOffering") + + KVMAgentCommands.StartVmCmd startCmd = null + env.afterSimulator(KVMConstant.KVM_START_VM_PATH) { KVMAgentCommands.StartVmResponse rsp, HttpEntity e -> + startCmd = JSONObjectUtil.toObject(e.body, KVMAgentCommands.StartVmCmd.class) + return rsp + } + + def vm = createVmInstance { + name = "test-pmu-x86" + imageUuid = image.uuid + l3NetworkUuids = [l3.uuid] + instanceOfferingUuid = instance.uuid + } as VmInstanceInventory + + assert startCmd != null + // On x86 (non-aarch64), PMU code path is not triggered, + // so StartVmCmd.pmu stays at its field default (true) + if ("x86_64".equals(vm.architecture) || vm.architecture == null) { + assert startCmd.pmu == true : "x86 VM should have PMU enabled by default" + } else if ("aarch64".equals(vm.architecture)) { + assert startCmd.pmu == false : "aarch64 VM should have PMU disabled by default" + } + + destroyVmInstance { uuid = vm.uuid } + expungeVmInstance { uuid = vm.uuid } + } + + void testPmuResourceConfigOverride() { + def image = env.inventoryByName("image1") + def l3 = env.inventoryByName("l3") + def instance = env.inventoryByName("instanceOffering") + + def vm = createVmInstance { + name = "test-pmu-override" + imageUuid = image.uuid + l3NetworkUuids = [l3.uuid] + instanceOfferingUuid = instance.uuid + } as VmInstanceInventory + + // Set vm.pmu=true via ResourceConfig (different from default false) + updateResourceConfig { + category = VmGlobalConfig.CATEGORY + name = VmGlobalConfig.VM_PMU.name + value = "true" + resourceUuid = vm.uuid + } + + KVMAgentCommands.StartVmCmd startCmd = null + env.afterSimulator(KVMConstant.KVM_START_VM_PATH) { KVMAgentCommands.StartVmResponse rsp, HttpEntity e -> + startCmd = JSONObjectUtil.toObject(e.body, KVMAgentCommands.StartVmCmd.class) + return rsp + } + + rebootVmInstance { uuid = vm.uuid } + + assert startCmd != null + // On x86, PMU stays true regardless of ResourceConfig (code only reads for aarch64) + // This verifies the ResourceConfig record exists and reboot doesn't crash + assert startCmd.pmu == true : "PMU should be true after reboot on x86" + + // Verify ResourceConfig was persisted + def configs = queryResourceConfig { + conditions = [ + "category=${VmGlobalConfig.CATEGORY}", + "name=${VmGlobalConfig.VM_PMU.name}", + "resourceUuid=${vm.uuid}" + ] + } + assert configs.size() == 1 : "ResourceConfig should be persisted" + + destroyVmInstance { uuid = vm.uuid } + expungeVmInstance { uuid = vm.uuid } + } +} diff --git a/test/src/test/groovy/org/zstack/test/integration/kvm/vm/migrate/LibvirtTlsMigrateCase.groovy b/test/src/test/groovy/org/zstack/test/integration/kvm/vm/migrate/LibvirtTlsMigrateCase.groovy new file mode 100644 index 00000000000..53bc0c46071 --- /dev/null +++ b/test/src/test/groovy/org/zstack/test/integration/kvm/vm/migrate/LibvirtTlsMigrateCase.groovy @@ -0,0 +1,299 @@ +package org.zstack.test.integration.kvm.vm.migrate + +import org.springframework.http.HttpEntity +import org.zstack.kvm.KVMAgentCommands +import org.zstack.kvm.KVMConstant +import org.zstack.kvm.KVMGlobalConfig +import org.zstack.kvm.KVMHost +import org.zstack.sdk.HostInventory +import org.zstack.sdk.UpdateGlobalConfigAction +import org.zstack.sdk.VmInstanceInventory +import org.zstack.test.integration.kvm.KvmTest +import org.zstack.testlib.EnvSpec +import org.zstack.testlib.SubCase +import org.zstack.testlib.Test +import org.zstack.utils.data.SizeUnit +import org.zstack.utils.gson.JSONObjectUtil + +/** + * Verify that the libvirt TLS configuration (ZSTAC-81343) is correctly + * propagated in the MigrateVmCmd sent to kvmagent. + * + * TLS certificate deployment is now handled by SSH-based detection + + * ansible deploy (ZSTAC-83696), which skips in unit tests. Only + * migration TLS flag propagation is tested here. + * + * Key logic under test (KVMHost.java): + * cmd.setUseTls(LIBVIRT_TLS_ENABLED && RECONNECT_HOST_RESTART_LIBVIRTD_SERVICE) + * cmd.setSrcHostManagementIp(srcHostMnIp) + */ +class LibvirtTlsMigrateCase extends SubCase { + EnvSpec env + + @Override + void clean() { + env.delete() + } + + @Override + void setup() { + useSpring(KvmTest.springSpec) + } + + @Override + void environment() { + env = env { + instanceOffering { + name = "instanceOffering" + memory = SizeUnit.GIGABYTE.toByte(8) + cpu = 4 + } + + zone { + name = "zone" + cluster { + name = "cluster" + hypervisorType = "KVM" + + kvm { + name = "kvm1" + managementIp = "127.0.0.1" + username = "root" + password = "password" + usedMem = 1000 + totalCpu = 10 + } + kvm { + name = "kvm2" + managementIp = "127.0.0.2" + username = "root" + password = "password" + usedMem = 1000 + totalCpu = 10 + } + + attachPrimaryStorage("ps") + attachL2Network("l2") + } + + l2NoVlanNetwork { + name = "l2" + physicalInterface = "eth0" + + l3Network { + name = "l3" + ip { + startIp = "192.168.100.10" + endIp = "192.168.100.100" + netmask = "255.255.255.0" + gateway = "192.168.100.1" + } + } + } + + cephPrimaryStorage { + name = "ps" + totalCapacity = SizeUnit.GIGABYTE.toByte(100) + availableCapacity = SizeUnit.GIGABYTE.toByte(100) + url = "ceph://pri" + fsid = "7ff218d9-f525-435f-8a40-3618d1772a64" + monUrls = ["root:password@localhost/?monPort=7777"] + } + + attachBackupStorage("bs") + } + + cephBackupStorage { + name = "bs" + totalCapacity = SizeUnit.GIGABYTE.toByte(100) + availableCapacity = SizeUnit.GIGABYTE.toByte(100) + url = "/bk" + fsid = "7ff218d9-f525-435f-8a40-3618d1772a64" + monUrls = ["root:password@localhost/?monPort=7777"] + + image { + name = "image" + url = "http://zstack.org/download/image.qcow2" + } + } + + vm { + name = "vm" + useCluster("cluster") + useHost("kvm1") + useL3Networks("l3") + useInstanceOffering("instanceOffering") + useImage("image") + } + } + } + + @Override + void test() { + env.create { + testSanIpParsing() + testMigrateWithTlsEnabled() + testMigrateWithTlsDisabled() + testMigrateWithRestartLibvirtdDisabled() + testGlobalConfigValidation() + } + } + + /** + * Case 1: Both LIBVIRT_TLS_ENABLED=true and RECONNECT_HOST_RESTART_LIBVIRTD_SERVICE=true + * => useTls should be true, srcHostManagementIp should be set + */ + void testMigrateWithTlsEnabled() { + def vm = env.inventoryByName("vm") as VmInstanceInventory + def host1 = env.inventoryByName("kvm1") as HostInventory + def host2 = env.inventoryByName("kvm2") as HostInventory + + // Ensure TLS is enabled (default is true) + KVMGlobalConfig.LIBVIRT_TLS_ENABLED.updateValue("true") + KVMGlobalConfig.RECONNECT_HOST_RESTART_LIBVIRTD_SERVICE.updateValue("true") + + KVMAgentCommands.MigrateVmCmd cmd = null + env.afterSimulator(KVMConstant.KVM_MIGRATE_VM_PATH) { rsp, HttpEntity e -> + cmd = JSONObjectUtil.toObject(e.body, KVMAgentCommands.MigrateVmCmd.class) + return rsp + } + + // Migrate vm from kvm1 to kvm2 + migrateVm { + vmInstanceUuid = vm.uuid + hostUuid = host2.uuid + } + + assert cmd != null : "MigrateVmCmd should have been captured" + assert cmd.useTls : "useTls should be true when both TLS and restartLibvirtd are enabled" + assert cmd.srcHostManagementIp == host1.managementIp : + "srcHostManagementIp should be source host management IP" + assert cmd.destHostManagementIp == host2.managementIp : + "destHostManagementIp should be dest host management IP" + } + + /** + * Case 2: LIBVIRT_TLS_ENABLED=false => useTls should be false regardless of restartLibvirtd + */ + void testMigrateWithTlsDisabled() { + def vm = env.inventoryByName("vm") as VmInstanceInventory + def host1 = env.inventoryByName("kvm1") as HostInventory + + // Disable TLS + KVMGlobalConfig.LIBVIRT_TLS_ENABLED.updateValue("false") + KVMGlobalConfig.RECONNECT_HOST_RESTART_LIBVIRTD_SERVICE.updateValue("true") + + KVMAgentCommands.MigrateVmCmd cmd = null + env.afterSimulator(KVMConstant.KVM_MIGRATE_VM_PATH) { rsp, HttpEntity e -> + cmd = JSONObjectUtil.toObject(e.body, KVMAgentCommands.MigrateVmCmd.class) + return rsp + } + + // Migrate back to kvm1 + migrateVm { + vmInstanceUuid = vm.uuid + hostUuid = host1.uuid + } + + assert cmd != null : "MigrateVmCmd should have been captured" + assert !cmd.useTls : "useTls should be false when TLS config is disabled" + + // Restore default + KVMGlobalConfig.LIBVIRT_TLS_ENABLED.updateValue("true") + } + + /** + * Case 3: LIBVIRT_TLS_ENABLED=true but RECONNECT_HOST_RESTART_LIBVIRTD_SERVICE=false + * => useTls should be false (AND logic: both must be true) + * + * This is a critical boundary: TLS config is on, but libvirtd was not restarted + * with TLS certs deployed, so we must NOT tell kvmagent to use TLS. + */ + void testMigrateWithRestartLibvirtdDisabled() { + def vm = env.inventoryByName("vm") as VmInstanceInventory + def host2 = env.inventoryByName("kvm2") as HostInventory + + KVMGlobalConfig.LIBVIRT_TLS_ENABLED.updateValue("true") + KVMGlobalConfig.RECONNECT_HOST_RESTART_LIBVIRTD_SERVICE.updateValue("false") + + KVMAgentCommands.MigrateVmCmd cmd = null + env.afterSimulator(KVMConstant.KVM_MIGRATE_VM_PATH) { rsp, HttpEntity e -> + cmd = JSONObjectUtil.toObject(e.body, KVMAgentCommands.MigrateVmCmd.class) + return rsp + } + + migrateVm { + vmInstanceUuid = vm.uuid + hostUuid = host2.uuid + } + + assert cmd != null : "MigrateVmCmd should have been captured" + assert !cmd.useTls : + "useTls should be false when restartLibvirtd is disabled (TLS certs not deployed)" + + // Restore default + KVMGlobalConfig.RECONNECT_HOST_RESTART_LIBVIRTD_SERVICE.updateValue("true") + } + + /** + * Case 4: Validate that libvirt.tls.enabled GlobalConfig only accepts true/false + */ + void testGlobalConfigValidation() { + // Valid values via SDK action + updateGlobalConfig { + category = "kvm" + name = "libvirt.tls.enabled" + value = "true" + } + assert KVMGlobalConfig.LIBVIRT_TLS_ENABLED.value(Boolean.class) == true + + updateGlobalConfig { + category = "kvm" + name = "libvirt.tls.enabled" + value = "false" + } + assert KVMGlobalConfig.LIBVIRT_TLS_ENABLED.value(Boolean.class) == false + + // Invalid value should be rejected + def action = new UpdateGlobalConfigAction() + action.category = "kvm" + action.name = "libvirt.tls.enabled" + action.value = "invalid" + action.sessionId = Test.currentEnvSpec.session.uuid + UpdateGlobalConfigAction.Result res = action.call() + assert res.error != null : "Setting an invalid value for libvirt.tls.enabled should fail" + + // Restore default + updateGlobalConfig { + category = "kvm" + name = "libvirt.tls.enabled" + value = "true" + } + } + + void testSanIpParsing() { + // typical openssl SAN output + def sanOutput = " IP Address:10.0.0.10, IP Address:192.168.1.1, DNS:host.example.com\n" + + def ips = KVMHost.parseSanIps(sanOutput) + assert ips.contains("10.0.0.10") + assert ips.contains("192.168.1.1") + assert ips.size() == 2 : "should only contain 2 IPs, got ${ips}" + + // prefix false-positive: 10.0.0.1 must NOT match when only 10.0.0.10 is in SAN + assert !ips.contains("10.0.0.1") : "10.0.0.1 should not match 10.0.0.10" + assert !ips.contains("192.168.1") : "partial IP should not match" + + // null / empty input + assert KVMHost.parseSanIps(null).isEmpty() + assert KVMHost.parseSanIps("").isEmpty() + + // multiline format + def multiline = "X509v3 Subject Alternative Name:\n IP Address:10.0.0.1\n IP Address:10.0.0.10\n" + def mlIps = KVMHost.parseSanIps(multiline) + assert mlIps.contains("10.0.0.1") + assert mlIps.contains("10.0.0.10") + assert mlIps.size() == 2 + assert !mlIps.contains("10.0.0") : "prefix should not match" + } +} diff --git a/test/src/test/groovy/org/zstack/test/integration/network/l2network/AttachL2NetworkCase.groovy b/test/src/test/groovy/org/zstack/test/integration/network/l2network/AttachL2NetworkCase.groovy index f31d19bae92..7d5e8bbfa1d 100644 --- a/test/src/test/groovy/org/zstack/test/integration/network/l2network/AttachL2NetworkCase.groovy +++ b/test/src/test/groovy/org/zstack/test/integration/network/l2network/AttachL2NetworkCase.groovy @@ -94,6 +94,7 @@ public class AttachL2NetworkCase extends SubCase{ @Override public void test() { env.create { + testCreateL2NetworkWithoutPhysicalInterface() testAttachL2NoVlanNetwork() testAttachL2ValnNetwork() testAttachL2NoVlanNetworkSynchronously() @@ -103,6 +104,36 @@ public class AttachL2NetworkCase extends SubCase{ } + void testCreateL2NetworkWithoutPhysicalInterface() { + ZoneInventory zone = env.inventoryByName("zone") + + // creating L2 NoVlan network without physicalInterface should fail when vSwitchType is LinuxBridge + expect(AssertionError.class) { + createL2NoVlanNetwork { + name = "test-no-physical-interface" + zoneUuid = zone.uuid + } + } + + // creating L2 NoVlan network with empty physicalInterface should also fail + expect(AssertionError.class) { + createL2NoVlanNetwork { + name = "test-empty-physical-interface" + zoneUuid = zone.uuid + physicalInterface = "" + } + } + + // creating L2 Vlan network without physicalInterface should fail when vSwitchType is LinuxBridge + expect(AssertionError.class) { + createL2VlanNetwork { + name = "test-vlan-no-physical-interface" + zoneUuid = zone.uuid + vlan = 100 + } + } + } + void testAttachL2NoVlanNetwork(){ L2NetworkInventory l21 = env.inventoryByName("l2-1") L2NetworkInventory l22 = env.inventoryByName("l2-2") diff --git a/test/src/test/groovy/org/zstack/test/integration/networkservice/provider/flat/FlatChangeVmIpOutsideCidrCase.groovy b/test/src/test/groovy/org/zstack/test/integration/networkservice/provider/flat/FlatChangeVmIpOutsideCidrCase.groovy new file mode 100644 index 00000000000..90875ddbf6a --- /dev/null +++ b/test/src/test/groovy/org/zstack/test/integration/networkservice/provider/flat/FlatChangeVmIpOutsideCidrCase.groovy @@ -0,0 +1,1219 @@ +package org.zstack.test.integration.networkservice.provider.flat + +import org.springframework.http.HttpEntity +import org.zstack.core.db.DatabaseFacade +import org.zstack.core.db.Q +import org.zstack.header.network.l3.UsedIpVO +import org.zstack.header.network.l3.UsedIpVO_ +import org.zstack.header.network.service.NetworkServiceType +import org.zstack.header.vm.VmInstanceVO +import org.zstack.header.vm.VmInstanceVO_ +import org.zstack.header.vm.VmNicVO +import org.zstack.header.vm.VmNicVO_ +import org.zstack.network.securitygroup.SecurityGroupConstant +import org.zstack.network.service.eip.EipConstant +import org.zstack.network.service.flat.FlatDhcpBackend +import org.zstack.network.service.flat.FlatNetworkServiceConstant +import org.zstack.network.service.userdata.UserdataConstant +import org.zstack.sdk.* +import org.zstack.test.integration.networkservice.provider.NetworkServiceProviderTest +import org.zstack.testlib.EnvSpec +import org.zstack.testlib.SubCase +import org.zstack.utils.data.SizeUnit +import org.zstack.utils.gson.JSONObjectUtil +import org.zstack.utils.network.IPv6Constants + +/** + * Test IP outside CIDR behavior for flat networks. + * Outside-range IPs are always allowed (no global config needed). + * + * Flat network combinations (3 combos): + * 1. flatL3_noRange_noDhcp — no IP range, no DHCP (enableIPAM=false, enableIpAddressAllocation()=false) + * 2. flatL3_range_noDhcp — has IP range, no DHCP (enableIPAM=true, enableIpAddressAllocation()=false) + * 3. flatL3_range_dhcp — has IP range, has DHCP (enableIPAM=true, enableIpAddressAllocation()=true) + * + * pubL3_range_dhcp is included only as VIP network for EIP tests. + * + * Each scenario tests: setVmStaticIp, changeVmNicNetwork, DHCP skip, EIP rejection. + * Additional: orphan IP backfill when adding IP range. + * + * Netmask/gateway auto-resolve tests (Case A–D): + * Uses flatL3_range_noDhcp (CIDR: 192.168.100.0/24) as destination. + * Tests the unified resolveIpv4NetmaskAndGateway logic via changeVmNicNetwork and setVmStaticIp. + * Case C (netmask mismatch) requires multi-NIC VMs via flatL3_second (no IPAM). + * Case D-3 (existing IP reuse) is unique to setVmStaticIp. + */ +class FlatChangeVmIpOutsideCidrCase extends SubCase { + + EnvSpec env + DatabaseFacade dbf + + @Override + void setup() { + useSpring(NetworkServiceProviderTest.springSpec) + } + + @Override + void clean() { + env.delete() + } + + @Override + void environment() { + env = env { + instanceOffering { + name = "instanceOffering" + memory = SizeUnit.GIGABYTE.toByte(1) + cpu = 1 + } + + sftpBackupStorage { + name = "sftp" + url = "/sftp" + username = "root" + password = "password" + hostname = "localhost" + + image { + name = "image1" + url = "http://zstack.org/download/test.qcow2" + } + } + + zone { + name = "zone" + description = "test" + + cluster { + name = "cluster" + hypervisorType = "KVM" + + kvm { + name = "kvm" + managementIp = "127.0.0.1" + username = "root" + password = "password" + } + + attachPrimaryStorage("local") + attachL2Network("l2-flat-noRange-noDhcp") + attachL2Network("l2-flat-range-noDhcp") + attachL2Network("l2-flat-range-dhcp") + attachL2Network("l2-pub-range-dhcp") + attachL2Network("l2-backfill") + attachL2Network("l2-dest") + attachL2Network("l2-second") + } + + localPrimaryStorage { + name = "local" + url = "/local_ps" + } + + // ========== Flat: no IP range, no DHCP ========== + l2NoVlanNetwork { + name = "l2-flat-noRange-noDhcp" + physicalInterface = "eth0" + + l3Network { + name = "flatL3_noRange_noDhcp" + enableIPAM = false + + service { + provider = FlatNetworkServiceConstant.FLAT_NETWORK_SERVICE_TYPE_STRING + types = [EipConstant.EIP_NETWORK_SERVICE_TYPE] + } + service { + provider = SecurityGroupConstant.SECURITY_GROUP_PROVIDER_TYPE + types = [SecurityGroupConstant.SECURITY_GROUP_NETWORK_SERVICE_TYPE] + } + // No IP range, no DHCP + } + } + + // ========== Flat: has IP range, no DHCP ========== + l2NoVlanNetwork { + name = "l2-flat-range-noDhcp" + physicalInterface = "eth1" + + l3Network { + name = "flatL3_range_noDhcp" + + service { + provider = FlatNetworkServiceConstant.FLAT_NETWORK_SERVICE_TYPE_STRING + types = [EipConstant.EIP_NETWORK_SERVICE_TYPE] + } + service { + provider = SecurityGroupConstant.SECURITY_GROUP_PROVIDER_TYPE + types = [SecurityGroupConstant.SECURITY_GROUP_NETWORK_SERVICE_TYPE] + } + + ip { + startIp = "192.168.100.10" + endIp = "192.168.100.200" + netmask = "255.255.255.0" + gateway = "192.168.100.1" + } + } + } + + // ========== Flat: has IP range, has DHCP ========== + l2NoVlanNetwork { + name = "l2-flat-range-dhcp" + physicalInterface = "eth2" + + l3Network { + name = "flatL3_range_dhcp" + + service { + provider = FlatNetworkServiceConstant.FLAT_NETWORK_SERVICE_TYPE_STRING + types = [NetworkServiceType.DHCP.toString(), + UserdataConstant.USERDATA_TYPE_STRING, + EipConstant.EIP_NETWORK_SERVICE_TYPE] + } + service { + provider = SecurityGroupConstant.SECURITY_GROUP_PROVIDER_TYPE + types = [SecurityGroupConstant.SECURITY_GROUP_NETWORK_SERVICE_TYPE] + } + + ip { + startIp = "192.168.200.10" + endIp = "192.168.200.200" + netmask = "255.255.255.0" + gateway = "192.168.200.1" + } + } + } + + // ========== Public: has IP range, has DHCP (VIP network for EIP tests only) ========== + l2NoVlanNetwork { + name = "l2-pub-range-dhcp" + physicalInterface = "eth4" + + l3Network { + name = "pubL3_range_dhcp" + category = "Public" + + service { + provider = FlatNetworkServiceConstant.FLAT_NETWORK_SERVICE_TYPE_STRING + types = [NetworkServiceType.DHCP.toString(), + EipConstant.EIP_NETWORK_SERVICE_TYPE] + } + service { + provider = SecurityGroupConstant.SECURITY_GROUP_PROVIDER_TYPE + types = [SecurityGroupConstant.SECURITY_GROUP_NETWORK_SERVICE_TYPE] + } + + ip { + startIp = "12.100.20.10" + endIp = "12.100.20.200" + netmask = "255.255.255.0" + gateway = "12.100.20.1" + } + } + } + + // ========== Dedicated L2/L3 for orphan IP backfill test ========== + l2NoVlanNetwork { + name = "l2-backfill" + physicalInterface = "eth5" + + l3Network { + name = "flatL3_backfill" + enableIPAM = false + + service { + provider = SecurityGroupConstant.SECURITY_GROUP_PROVIDER_TYPE + types = [SecurityGroupConstant.SECURITY_GROUP_NETWORK_SERVICE_TYPE] + } + // No IP range initially, no DHCP + } + } + + // ========== Destination L3 for changeVmNicNetwork tests ========== + l2NoVlanNetwork { + name = "l2-dest" + physicalInterface = "eth6" + + l3Network { + name = "flatL3_dest" + enableIPAM = false + + service { + provider = SecurityGroupConstant.SECURITY_GROUP_PROVIDER_TYPE + types = [SecurityGroupConstant.SECURITY_GROUP_NETWORK_SERVICE_TYPE] + } + // No IP range, no DHCP — used as changeVmNicNetwork source + } + } + + // ========== Second L3: no IPAM, used as extra NIC for multi-NIC resolve tests ========== + l2NoVlanNetwork { + name = "l2-second" + physicalInterface = "eth7" + + l3Network { + name = "flatL3_second" + enableIPAM = false + + service { + provider = SecurityGroupConstant.SECURITY_GROUP_PROVIDER_TYPE + types = [SecurityGroupConstant.SECURITY_GROUP_NETWORK_SERVICE_TYPE] + } + } + } + + attachBackupStorage("sftp") + } + } + } + + @Override + void test() { + dbf = bean(DatabaseFacade.class) + env.create { + // ========================================== + // Outside-range IPs are always allowed (no global config needed) + // ========================================== + + // --- Flat: no IP range, no DHCP --- + testSetStaticIp_flatNoRangeNoDhcp() + testChangeNicNetwork_flatNoRangeNoDhcp() + testDhcpSkip_flatNoRangeNoDhcp() + testEipReject_flatNoRangeNoDhcp() + + // --- Flat: has IP range, no DHCP --- + testSetStaticIp_flatRangeNoDhcp() + testChangeNicNetwork_flatRangeNoDhcp() + testDhcpSkip_flatRangeNoDhcp() + testEipReject_flatRangeNoDhcp() + + // --- Flat: has IP range, has DHCP --- + testSetStaticIp_flatRangeDhcp() + testChangeNicNetwork_flatRangeDhcp() + testDhcpSkip_flatRangeDhcp() + testEipReject_flatRangeDhcp() + + // --- NIC DNS priority --- + testNicDnsPreservedWhenApiOmitsDns_flatRangeDhcp() + testNicDnsRemovedWhenEmptyDnsList_flatRangeDhcp() + + + // ========================================== + // Orphan IP backfill + // ========================================== + testOrphanIpBackfillOnAddIpRange() + + // ========================================== + // Netmask/gateway auto-resolve (Case A–D) + // Uses flatL3_range_noDhcp (CIDR 192.168.100.0/24) + // ========================================== + testResolve_A1_bothProvided_gatewayInCidr_success() + testResolve_A2_bothProvided_gatewayNotInCidr_error() + testResolve_B1_gatewayAndIpBothInCidr_success() + testResolve_B2_ipInCidrButGatewayNotInCidr_error() + testResolve_B3_ipNotInAnyCidr_error() + testResolve_C1_netmaskMatchesCidr_success() + testResolve_C4_netmaskMismatch_nonDefaultNonSole_success() + testResolve_D1_ipInCidr_success() + testResolve_D2_ipOutsideCidr_error() + testResolve_D3_setStaticIp_existingIpReuse_success() + } + } + + + // ================================================================ + // Helper: create a VM on a given L3 + // ================================================================ + + VmInstanceInventory createVmOnL3(String vmName, String l3Uuid) { + return createVmInstance { + name = vmName + imageUuid = env.inventoryByName("image1").uuid + instanceOfferingUuid = env.inventoryByName("instanceOffering").uuid + l3NetworkUuids = [l3Uuid] + } + } + + VmInstanceInventory createVmOnL3(String vmName, List l3Uuids, String defaultL3Uuid = null) { + return createVmInstance { + name = vmName + imageUuid = env.inventoryByName("image1").uuid + instanceOfferingUuid = env.inventoryByName("instanceOffering").uuid + l3NetworkUuids = l3Uuids + if (defaultL3Uuid != null) { + delegate.defaultL3NetworkUuid = defaultL3Uuid + } + } + } + + // ================================================================ + // Flat: no IP range, no DHCP + // ================================================================ + + /** + * Flat/no-range/no-DHCP: setVmStaticIp with outside-range IP should succeed. + * Must provide netmask/gateway explicitly (no IP range to default from). + */ + void testSetStaticIp_flatNoRangeNoDhcp() { + L3NetworkInventory l3 = env.inventoryByName("flatL3_noRange_noDhcp") + + VmInstanceInventory vm = createVmOnL3("vm-flat-noRange-noDhcp-set", l3.uuid) + + setVmStaticIp { + vmInstanceUuid = vm.uuid + l3NetworkUuid = l3.uuid + ip = "172.16.0.50" + netmask = "255.255.0.0" + gateway = "172.16.0.1" + } + + // Verify UsedIpVO + UsedIpVO usedIp = Q.New(UsedIpVO.class) + .eq(UsedIpVO_.vmNicUuid, vm.vmNics[0].uuid) + .eq(UsedIpVO_.ip, "172.16.0.50") + .find() + assert usedIp != null + assert usedIp.ipRangeUuid == null : "ipRangeUuid should be null for outside-range IP" + assert usedIp.netmask == "255.255.0.0" + assert usedIp.gateway == "172.16.0.1" + + // Verify VmNicVO + VmNicVO nicVO = dbFindByUuid(vm.vmNics[0].uuid, VmNicVO.class) + assert nicVO.ip == "172.16.0.50" + assert nicVO.netmask == "255.255.0.0" + assert nicVO.gateway == "172.16.0.1" + } + + /** + * Flat/no-range/no-DHCP: changeVmNicNetwork with outside-range IP should succeed. + */ + void testChangeNicNetwork_flatNoRangeNoDhcp() { + L3NetworkInventory l3 = env.inventoryByName("flatL3_noRange_noDhcp") + L3NetworkInventory srcL3 = env.inventoryByName("flatL3_dest") + + VmInstanceInventory vm = createVmOnL3("vm-flat-noRange-noDhcp-change", srcL3.uuid) + VmNicInventory vmNic = vm.vmNics[0] + + changeVmNicNetwork { + vmNicUuid = vmNic.uuid + destL3NetworkUuid = l3.uuid + systemTags = [ + String.format("staticIp::%s::172.16.0.60", l3.uuid), + String.format("ipv4Netmask::%s::255.255.0.0", l3.uuid), + String.format("ipv4Gateway::%s::172.16.0.1", l3.uuid) + ] + } + + VmNicVO nicVO = dbFindByUuid(vmNic.uuid, VmNicVO.class) + assert nicVO.l3NetworkUuid == l3.uuid + assert nicVO.ip == "172.16.0.60" + + UsedIpVO usedIp = Q.New(UsedIpVO.class) + .eq(UsedIpVO_.vmNicUuid, vmNic.uuid) + .eq(UsedIpVO_.ip, "172.16.0.60") + .find() + assert usedIp != null + assert usedIp.ipRangeUuid == null : "ipRangeUuid should be null for outside-range IP" + } + + /** + * Flat/no-range/no-DHCP: outside-range IP should NOT appear in DHCP messages on reboot. + */ + void testDhcpSkip_flatNoRangeNoDhcp() { + VmInstanceInventory vm = queryVmInstance { conditions = ["name=vm-flat-noRange-noDhcp-set"] }[0] + + stopVmInstance { uuid = vm.uuid } + + boolean dhcpApplied = false + env.afterSimulator(FlatDhcpBackend.APPLY_DHCP_PATH) { rsp, HttpEntity e -> + FlatDhcpBackend.ApplyDhcpCmd cmd = JSONObjectUtil.toObject(e.body, FlatDhcpBackend.ApplyDhcpCmd.class) + for (def dhcp : cmd.dhcp) { + if (dhcp.ip == "172.16.0.50") { dhcpApplied = true } + } + return rsp + } + env.afterSimulator(FlatDhcpBackend.BATCH_APPLY_DHCP_PATH) { rsp, HttpEntity e -> + FlatDhcpBackend.BatchApplyDhcpCmd cmd = JSONObjectUtil.toObject(e.body, FlatDhcpBackend.BatchApplyDhcpCmd.class) + for (def dhcpInfo : cmd.dhcpInfos) { + for (def dhcp : dhcpInfo.dhcp) { + if (dhcp.ip == "172.16.0.50") { dhcpApplied = true } + } + } + return rsp + } + + startVmInstance { uuid = vm.uuid } + + assert !dhcpApplied : "DHCP should NOT include outside-range IP 172.16.0.50 on no-DHCP L3" + } + + + /** + * Flat/no-range/no-DHCP: EIP should reject binding to NIC with outside-range IP. + */ + void testEipReject_flatNoRangeNoDhcp() { + L3NetworkInventory pubL3 = env.inventoryByName("pubL3_range_dhcp") + VmInstanceInventory vm = queryVmInstance { conditions = ["name=vm-flat-noRange-noDhcp-set"] }[0] + L3NetworkInventory l3 = env.inventoryByName("flatL3_noRange_noDhcp") + VmNicInventory nic = vm.vmNics.find { it.l3NetworkUuid == l3.uuid } + assert nic != null + + VipInventory vip = createVip { + name = "vip-flat-noRange-noDhcp" + l3NetworkUuid = pubL3.uuid + } + EipInventory eip = createEip { + name = "eip-flat-noRange-noDhcp" + vipUuid = vip.uuid + } + + expect(AssertionError.class) { + attachEip { + eipUuid = eip.uuid + vmNicUuid = nic.uuid + } + } + } + + // ================================================================ + // Part 2: Global config ON — Flat: has IP range, no DHCP + // ================================================================ + + /** + * Flat/range/no-DHCP: outside-range IP should succeed; in-range IP also works. + */ + void testSetStaticIp_flatRangeNoDhcp() { + L3NetworkInventory l3 = env.inventoryByName("flatL3_range_noDhcp") + + VmInstanceInventory vm = createVmOnL3("vm-flat-range-noDhcp-set", l3.uuid) + + // Outside-range IP should succeed + setVmStaticIp { + vmInstanceUuid = vm.uuid + l3NetworkUuid = l3.uuid + ip = "10.0.0.50" + netmask = "255.255.255.0" + gateway = "10.0.0.1" + } + + UsedIpVO usedIp = Q.New(UsedIpVO.class) + .eq(UsedIpVO_.vmNicUuid, vm.vmNics[0].uuid) + .eq(UsedIpVO_.ip, "10.0.0.50") + .find() + assert usedIp != null + assert usedIp.ipRangeUuid == null : "ipRangeUuid should be null for outside-range IP" + assert usedIp.netmask == "255.255.255.0" + assert usedIp.gateway == "10.0.0.1" + + VmNicVO nicVO = dbFindByUuid(vm.vmNics[0].uuid, VmNicVO.class) + assert nicVO.ip == "10.0.0.50" + } + + /** + * Flat/range/no-DHCP: changeVmNicNetwork with outside-range IP should succeed. + */ + void testChangeNicNetwork_flatRangeNoDhcp() { + L3NetworkInventory l3 = env.inventoryByName("flatL3_range_noDhcp") + L3NetworkInventory srcL3 = env.inventoryByName("flatL3_dest") + + VmInstanceInventory vm = createVmOnL3("vm-flat-range-noDhcp-change", srcL3.uuid) + VmNicInventory vmNic = vm.vmNics[0] + + changeVmNicNetwork { + vmNicUuid = vmNic.uuid + destL3NetworkUuid = l3.uuid + systemTags = [ + String.format("staticIp::%s::10.0.0.60", l3.uuid), + String.format("ipv4Netmask::%s::255.255.255.0", l3.uuid), + String.format("ipv4Gateway::%s::10.0.0.1", l3.uuid) + ] + } + + VmNicVO nicVO = dbFindByUuid(vmNic.uuid, VmNicVO.class) + assert nicVO.l3NetworkUuid == l3.uuid + assert nicVO.ip == "10.0.0.60" + + UsedIpVO usedIp = Q.New(UsedIpVO.class) + .eq(UsedIpVO_.vmNicUuid, vmNic.uuid) + .eq(UsedIpVO_.ip, "10.0.0.60") + .find() + assert usedIp != null + assert usedIp.ipRangeUuid == null : "ipRangeUuid should be null for outside-range IP" + } + + /** + * Flat/range/no-DHCP: outside-range IP should NOT appear in DHCP messages on reboot. + */ + void testDhcpSkip_flatRangeNoDhcp() { + VmInstanceInventory vm = queryVmInstance { conditions = ["name=vm-flat-range-noDhcp-set"] }[0] + + stopVmInstance { uuid = vm.uuid } + + boolean dhcpApplied = false + env.afterSimulator(FlatDhcpBackend.APPLY_DHCP_PATH) { rsp, HttpEntity e -> + FlatDhcpBackend.ApplyDhcpCmd cmd = JSONObjectUtil.toObject(e.body, FlatDhcpBackend.ApplyDhcpCmd.class) + for (def dhcp : cmd.dhcp) { + if (dhcp.ip == "10.0.0.50") { dhcpApplied = true } + } + return rsp + } + env.afterSimulator(FlatDhcpBackend.BATCH_APPLY_DHCP_PATH) { rsp, HttpEntity e -> + FlatDhcpBackend.BatchApplyDhcpCmd cmd = JSONObjectUtil.toObject(e.body, FlatDhcpBackend.BatchApplyDhcpCmd.class) + for (def dhcpInfo : cmd.dhcpInfos) { + for (def dhcp : dhcpInfo.dhcp) { + if (dhcp.ip == "10.0.0.50") { dhcpApplied = true } + } + } + return rsp + } + + startVmInstance { uuid = vm.uuid } + + assert !dhcpApplied : "DHCP should NOT include outside-range IP 10.0.0.50 on no-DHCP L3" + } + + + /** + * Flat/range/no-DHCP: EIP should reject binding to NIC with outside-range IP. + */ + void testEipReject_flatRangeNoDhcp() { + L3NetworkInventory pubL3 = env.inventoryByName("pubL3_range_dhcp") + VmInstanceInventory vm = queryVmInstance { conditions = ["name=vm-flat-range-noDhcp-set"] }[0] + L3NetworkInventory l3 = env.inventoryByName("flatL3_range_noDhcp") + VmNicInventory nic = vm.vmNics.find { it.l3NetworkUuid == l3.uuid } + assert nic != null + + VipInventory vip = createVip { + name = "vip-flat-range-noDhcp" + l3NetworkUuid = pubL3.uuid + } + EipInventory eip = createEip { + name = "eip-flat-range-noDhcp" + vipUuid = vip.uuid + } + + expect(AssertionError.class) { + attachEip { + eipUuid = eip.uuid + vmNicUuid = nic.uuid + } + } + } + + // ================================================================ + // Part 2: Global config ON — Flat: has IP range, has DHCP + // ================================================================ + + /** + * Flat/range/DHCP: outside-range IP should succeed with global config ON; + * in-range IP also works normally. + */ + void testSetStaticIp_flatRangeDhcp() { + L3NetworkInventory l3 = env.inventoryByName("flatL3_range_dhcp") + + VmInstanceInventory vm = createVmOnL3("vm-flat-range-dhcp-set", l3.uuid) + + // Outside-range IP should succeed with global config ON + setVmStaticIp { + vmInstanceUuid = vm.uuid + l3NetworkUuid = l3.uuid + ip = "10.0.1.50" + netmask = "255.255.255.0" + gateway = "10.0.1.1" + } + + UsedIpVO usedIp = Q.New(UsedIpVO.class) + .eq(UsedIpVO_.vmNicUuid, vm.vmNics[0].uuid) + .eq(UsedIpVO_.ip, "10.0.1.50") + .find() + assert usedIp != null + assert usedIp.ipRangeUuid == null : "ipRangeUuid should be null for outside-range IP" + assert usedIp.netmask == "255.255.255.0" + assert usedIp.gateway == "10.0.1.1" + + VmNicVO nicVO = dbFindByUuid(vm.vmNics[0].uuid, VmNicVO.class) + assert nicVO.ip == "10.0.1.50" + } + + /** + * Flat/range/DHCP: changeVmNicNetwork with outside-range IP should succeed. + */ + void testChangeNicNetwork_flatRangeDhcp() { + L3NetworkInventory l3 = env.inventoryByName("flatL3_range_dhcp") + L3NetworkInventory srcL3 = env.inventoryByName("flatL3_dest") + + VmInstanceInventory vm = createVmOnL3("vm-flat-range-dhcp-change", srcL3.uuid) + VmNicInventory vmNic = vm.vmNics[0] + + changeVmNicNetwork { + vmNicUuid = vmNic.uuid + destL3NetworkUuid = l3.uuid + systemTags = [ + String.format("staticIp::%s::10.0.1.60", l3.uuid), + String.format("ipv4Netmask::%s::255.255.255.0", l3.uuid), + String.format("ipv4Gateway::%s::10.0.1.1", l3.uuid) + ] + } + + VmNicVO nicVO = dbFindByUuid(vmNic.uuid, VmNicVO.class) + assert nicVO.l3NetworkUuid == l3.uuid + assert nicVO.ip == "10.0.1.60" + + UsedIpVO usedIp = Q.New(UsedIpVO.class) + .eq(UsedIpVO_.vmNicUuid, vmNic.uuid) + .eq(UsedIpVO_.ip, "10.0.1.60") + .find() + assert usedIp != null + assert usedIp.ipRangeUuid == null : "ipRangeUuid should be null for outside-range IP" + } + + /** + * Flat/range/DHCP: outside-range IP should NOT appear in DHCP messages on reboot, + * even though DHCP service is enabled on this L3. + */ + void testDhcpSkip_flatRangeDhcp() { + VmInstanceInventory vm = queryVmInstance { conditions = ["name=vm-flat-range-dhcp-set"] }[0] + + stopVmInstance { uuid = vm.uuid } + + boolean dhcpAppliedForOutsideIp = false + env.afterSimulator(FlatDhcpBackend.APPLY_DHCP_PATH) { rsp, HttpEntity e -> + FlatDhcpBackend.ApplyDhcpCmd cmd = JSONObjectUtil.toObject(e.body, FlatDhcpBackend.ApplyDhcpCmd.class) + for (def dhcp : cmd.dhcp) { + if (dhcp.ip == "10.0.1.50") { dhcpAppliedForOutsideIp = true } + } + return rsp + } + env.afterSimulator(FlatDhcpBackend.BATCH_APPLY_DHCP_PATH) { rsp, HttpEntity e -> + FlatDhcpBackend.BatchApplyDhcpCmd cmd = JSONObjectUtil.toObject(e.body, FlatDhcpBackend.BatchApplyDhcpCmd.class) + for (def dhcpInfo : cmd.dhcpInfos) { + for (def dhcp : dhcpInfo.dhcp) { + if (dhcp.ip == "10.0.1.50") { dhcpAppliedForOutsideIp = true } + } + } + return rsp + } + + startVmInstance { uuid = vm.uuid } + + assert !dhcpAppliedForOutsideIp : "DHCP should NOT include outside-range IP 10.0.1.50 even on DHCP-enabled L3" + } + + + /** + * Flat/range/DHCP: EIP should reject binding to NIC with outside-range IP. + */ + void testEipReject_flatRangeDhcp() { + L3NetworkInventory pubL3 = env.inventoryByName("pubL3_range_dhcp") + VmInstanceInventory vm = queryVmInstance { conditions = ["name=vm-flat-range-dhcp-set"] }[0] + L3NetworkInventory l3 = env.inventoryByName("flatL3_range_dhcp") + VmNicInventory nic = vm.vmNics.find { it.l3NetworkUuid == l3.uuid } + assert nic != null + + VipInventory vip = createVip { + name = "vip-flat-range-dhcp" + l3NetworkUuid = pubL3.uuid + } + EipInventory eip = createEip { + name = "eip-flat-range-dhcp" + vipUuid = vip.uuid + } + + expect(AssertionError.class) { + attachEip { + eipUuid = eip.uuid + vmNicUuid = nic.uuid + } + } + } + + // ================================================================ + // NIC DNS priority: NIC DNS > L3 DNS + // ================================================================ + + /** + * When setVmStaticIp is called with dnsAddresses, NIC-level DNS is used in DHCP. + * When setVmStaticIp is called again WITHOUT dnsAddresses (null), old NIC DNS is preserved. + */ + void testNicDnsPreservedWhenApiOmitsDns_flatRangeDhcp() { + L3NetworkInventory l3 = env.inventoryByName("flatL3_range_dhcp") + + // Add L3-level DNS + addDnsToL3Network { + l3NetworkUuid = l3.uuid + dns = "8.8.8.8" + } + + // Create a VM on the DHCP-enabled L3 + VmInstanceInventory vm = createVmOnL3("vm-nic-dns-preserve", l3.uuid) + + // Set NIC-level DNS via setVmStaticIp + List freeIps = getFreeIp { + l3NetworkUuid = l3.uuid + ipVersion = IPv6Constants.IPv4 + limit = 1 + } as List + String ip1 = freeIps.get(0).getIp() + + setVmStaticIp { + vmInstanceUuid = vm.uuid + l3NetworkUuid = l3.uuid + ip = ip1 + dnsAddresses = ["1.1.1.1", "1.0.0.1"] + } + + // Intercept DHCP apply and reboot to trigger DHCP re-apply + FlatDhcpBackend.BatchApplyDhcpCmd cmd1 = null + env.afterSimulator(FlatDhcpBackend.BATCH_APPLY_DHCP_PATH) { rsp, HttpEntity e -> + cmd1 = JSONObjectUtil.toObject(e.body, FlatDhcpBackend.BatchApplyDhcpCmd.class) + return rsp + } + + rebootVmInstance { uuid = vm.uuid } + + assert cmd1 != null + def dhcp1 = cmd1.dhcpInfos.collectMany { it.dhcp }.find { it.ip == ip1 } + assert dhcp1 != null : "DHCP entry for IP ${ip1} should exist" + assert dhcp1.dns == ["1.1.1.1", "1.0.0.1"] : "NIC DNS should override L3 DNS, got: ${dhcp1.dns}" + + // Now call setVmStaticIp again WITHOUT dnsAddresses (only change IP) + // dnsAddresses is null => old NIC DNS should be preserved + List freeIps2 = getFreeIp { + l3NetworkUuid = l3.uuid + ipVersion = IPv6Constants.IPv4 + limit = 1 + } as List + String ip2 = freeIps2.get(0).getIp() + + setVmStaticIp { + vmInstanceUuid = vm.uuid + l3NetworkUuid = l3.uuid + ip = ip2 + // dnsAddresses is NOT set => null in the message + } + + FlatDhcpBackend.BatchApplyDhcpCmd cmd2 = null + env.afterSimulator(FlatDhcpBackend.BATCH_APPLY_DHCP_PATH) { rsp, HttpEntity e -> + cmd2 = JSONObjectUtil.toObject(e.body, FlatDhcpBackend.BatchApplyDhcpCmd.class) + return rsp + } + + rebootVmInstance { uuid = vm.uuid } + + assert cmd2 != null + def dhcp2 = cmd2.dhcpInfos.collectMany { it.dhcp }.find { it.ip == ip2 } + assert dhcp2 != null : "DHCP entry for IP ${ip2} should exist" + assert dhcp2.dns == ["1.1.1.1", "1.0.0.1"] : "NIC DNS should be preserved when API omits dnsAddresses, got: ${dhcp2.dns}" + } + + /** + * When setVmStaticIp is called with an empty dnsAddresses list ([]), + * NIC DNS is removed and DHCP falls back to L3 network DNS. + */ + void testNicDnsRemovedWhenEmptyDnsList_flatRangeDhcp() { + L3NetworkInventory l3 = env.inventoryByName("flatL3_range_dhcp") + + // The previous test already added "8.8.8.8" as L3 DNS and created "vm-nic-dns-preserve" + // with NIC DNS ["1.1.1.1", "1.0.0.1"]. Reuse that VM. + VmInstanceInventory vm = queryVmInstance { conditions = ["name=vm-nic-dns-preserve"] }[0] + + // Must use a different IP because setVmStaticIp rejects the same IP + List freeIps = getFreeIp { + l3NetworkUuid = l3.uuid + ipVersion = IPv6Constants.IPv4 + limit = 1 + } as List + String newIp = freeIps.get(0).getIp() + + // Call setVmStaticIp with a new IP and explicit empty dnsAddresses => removes NIC DNS + setVmStaticIp { + vmInstanceUuid = vm.uuid + l3NetworkUuid = l3.uuid + ip = newIp + dnsAddresses = [] + } + + // Intercept DHCP apply and reboot + FlatDhcpBackend.BatchApplyDhcpCmd cmd = null + env.afterSimulator(FlatDhcpBackend.BATCH_APPLY_DHCP_PATH) { rsp, HttpEntity e -> + cmd = JSONObjectUtil.toObject(e.body, FlatDhcpBackend.BatchApplyDhcpCmd.class) + return rsp + } + + rebootVmInstance { uuid = vm.uuid } + + assert cmd != null + def dhcp = cmd.dhcpInfos.collectMany { it.dhcp }.find { it.ip == newIp } + assert dhcp != null : "DHCP entry for IP ${newIp} should exist" + assert dhcp.dns.contains("8.8.8.8") : "After removing NIC DNS, DHCP should fall back to L3 DNS (8.8.8.8), got: ${dhcp.dns}" + assert !dhcp.dns.contains("1.1.1.1") : "NIC DNS 1.1.1.1 should no longer be present after clearing, got: ${dhcp.dns}" + assert !dhcp.dns.contains("1.0.0.1") : "NIC DNS 1.0.0.1 should no longer be present after clearing, got: ${dhcp.dns}" + } + + + // ================================================================ + // Part 3: Orphan IP backfill (global config ON) + // ================================================================ + + /** + * Create orphan IPs on flatL3_backfill (no IP range), + * then add an IP range covering the orphan IPs. + * Verify ipRangeUuid is backfilled and capacity increases. + */ + void testOrphanIpBackfillOnAddIpRange() { + L3NetworkInventory backfillL3 = env.inventoryByName("flatL3_backfill") + + // Step 1: create VMs and assign outside-range IPs + VmInstanceInventory orphanVm1 = createVmOnL3("vm-backfill-orphan-1", backfillL3.uuid) + setVmStaticIp { + vmInstanceUuid = orphanVm1.uuid + l3NetworkUuid = backfillL3.uuid + ip = "172.16.0.80" + netmask = "255.255.0.0" + gateway = "172.16.0.1" + } + + VmInstanceInventory orphanVm2 = createVmOnL3("vm-backfill-orphan-2", backfillL3.uuid) + setVmStaticIp { + vmInstanceUuid = orphanVm2.uuid + l3NetworkUuid = backfillL3.uuid + ip = "172.16.0.90" + netmask = "255.255.0.0" + gateway = "172.16.0.1" + } + + // Confirm orphan IPs exist + long outsideCount = Q.New(UsedIpVO.class) + .eq(UsedIpVO_.l3NetworkUuid, backfillL3.uuid) + .isNull(UsedIpVO_.ipRangeUuid) + .count() + assert outsideCount == 2 : "There should be 2 orphan IPs on flatL3_backfill" + + // Step 2: record capacity before backfill + GetIpAddressCapacityResult beforeBackfill = getIpAddressCapacity { + l3NetworkUuids = [backfillL3.uuid] + } + + // Step 3: add IP range covering the orphan IPs (172.16.0.80, 172.16.0.90) + IpRangeInventory ipRange = addIpRange { + delegate.name = "backfill-ip-range" + delegate.l3NetworkUuid = backfillL3.uuid + delegate.startIp = "172.16.0.2" + delegate.endIp = "172.16.0.253" + delegate.gateway = "172.16.0.1" + delegate.netmask = "255.255.0.0" + } + + // Step 4: verify orphan IPs now have ipRangeUuid backfilled + long backfilledCount = Q.New(UsedIpVO.class) + .eq(UsedIpVO_.l3NetworkUuid, backfillL3.uuid) + .eq(UsedIpVO_.ipRangeUuid, ipRange.uuid) + .count() + assert backfilledCount == outsideCount : + "all ${outsideCount} orphan IPs should now be associated with the new IP range" + + // No more orphan IPs + long remainingOrphanCount = Q.New(UsedIpVO.class) + .eq(UsedIpVO_.l3NetworkUuid, backfillL3.uuid) + .isNull(UsedIpVO_.ipRangeUuid) + .count() + assert remainingOrphanCount == 0 : "all orphan IPs should now have ipRangeUuid" + + // Step 5: capacity should now include the backfilled IPs + GetIpAddressCapacityResult afterBackfill = getIpAddressCapacity { + l3NetworkUuids = [backfillL3.uuid] + } + assert afterBackfill.totalCapacity > beforeBackfill.totalCapacity : + "totalCapacity should increase after adding new IP range" + assert afterBackfill.usedIpAddressNumber == beforeBackfill.usedIpAddressNumber + outsideCount : + "usedIpAddressNumber should increase by ${outsideCount} after backfill" + } + + // ================================================================ + // Netmask/gateway auto-resolve tests (Case A–D) + // Uses flatL3_range_noDhcp (CIDR: 192.168.100.0/24, gw: 192.168.100.1) + // Source L3: flatL3_dest (no IPAM) + // Second L3: flatL3_second (no IPAM, for multi-NIC tests) + // ================================================================ + + /** + * a-1: both netmask+gateway provided, gateway in CIDR(ip/netmask). + * Expected: success, use user input as-is. + */ + void testResolve_A1_bothProvided_gatewayInCidr_success() { + L3NetworkInventory destL3 = env.inventoryByName("flatL3_range_noDhcp") + L3NetworkInventory srcL3 = env.inventoryByName("flatL3_dest") + + VmInstanceInventory vm = createVmOnL3("vm-resolve-a1", srcL3.uuid) + VmNicInventory vmNic = vm.vmNics[0] + + changeVmNicNetwork { + vmNicUuid = vmNic.uuid + destL3NetworkUuid = destL3.uuid + systemTags = [ + String.format("staticIp::%s::192.168.100.40", destL3.uuid), + String.format("ipv4Netmask::%s::255.255.255.0", destL3.uuid), + String.format("ipv4Gateway::%s::192.168.100.1", destL3.uuid) + ] + } + + VmNicVO nicVO = dbFindByUuid(vmNic.uuid, VmNicVO.class) + assert nicVO.l3NetworkUuid == destL3.uuid + assert nicVO.ip == "192.168.100.40" + assert nicVO.netmask == "255.255.255.0" + assert nicVO.gateway == "192.168.100.1" + + UsedIpVO usedIp = Q.New(UsedIpVO.class) + .eq(UsedIpVO_.vmNicUuid, vmNic.uuid) + .eq(UsedIpVO_.ip, "192.168.100.40") + .find() + assert usedIp != null + assert usedIp.netmask == "255.255.255.0" + assert usedIp.gateway == "192.168.100.1" + } + + /** + * a-2: both netmask+gateway provided, gateway NOT in CIDR(ip/netmask). + * Expected: error. + */ + void testResolve_A2_bothProvided_gatewayNotInCidr_error() { + L3NetworkInventory destL3 = env.inventoryByName("flatL3_range_noDhcp") + L3NetworkInventory srcL3 = env.inventoryByName("flatL3_dest") + + VmInstanceInventory vm = createVmOnL3("vm-resolve-a2", srcL3.uuid) + VmNicInventory vmNic = vm.vmNics[0] + + expect(AssertionError.class) { + changeVmNicNetwork { + vmNicUuid = vmNic.uuid + destL3NetworkUuid = destL3.uuid + systemTags = [ + String.format("staticIp::%s::192.168.100.41", destL3.uuid), + String.format("ipv4Netmask::%s::255.255.255.0", destL3.uuid), + String.format("ipv4Gateway::%s::10.0.0.1", destL3.uuid) + ] + } + } + } + + /** + * b-1: gateway provided (no netmask), IP and gateway both in L3 CIDR. + * Expected: success, netmask from CIDR. + */ + void testResolve_B1_gatewayAndIpBothInCidr_success() { + L3NetworkInventory destL3 = env.inventoryByName("flatL3_range_noDhcp") + L3NetworkInventory srcL3 = env.inventoryByName("flatL3_dest") + + VmInstanceInventory vm = createVmOnL3("vm-resolve-b1", srcL3.uuid) + VmNicInventory vmNic = vm.vmNics[0] + + changeVmNicNetwork { + vmNicUuid = vmNic.uuid + destL3NetworkUuid = destL3.uuid + systemTags = [ + String.format("staticIp::%s::192.168.100.50", destL3.uuid), + String.format("ipv4Gateway::%s::192.168.100.1", destL3.uuid) + ] + } + + VmNicVO nicVO = dbFindByUuid(vmNic.uuid, VmNicVO.class) + assert nicVO.ip == "192.168.100.50" + assert nicVO.netmask == "255.255.255.0" : "netmask should be inferred from L3 CIDR" + assert nicVO.gateway == "192.168.100.1" + } + + /** + * b-2: gateway provided (no netmask), IP in CIDR but gateway NOT in CIDR. + * Expected: error. + */ + void testResolve_B2_ipInCidrButGatewayNotInCidr_error() { + L3NetworkInventory destL3 = env.inventoryByName("flatL3_range_noDhcp") + L3NetworkInventory srcL3 = env.inventoryByName("flatL3_dest") + + VmInstanceInventory vm = createVmOnL3("vm-resolve-b2", srcL3.uuid) + VmNicInventory vmNic = vm.vmNics[0] + + expect(AssertionError.class) { + changeVmNicNetwork { + vmNicUuid = vmNic.uuid + destL3NetworkUuid = destL3.uuid + systemTags = [ + String.format("staticIp::%s::192.168.100.51", destL3.uuid), + String.format("ipv4Gateway::%s::10.0.0.1", destL3.uuid) + ] + } + } + } + + /** + * b-3: gateway provided (no netmask), IP NOT in any CIDR. + * Expected: error. + */ + void testResolve_B3_ipNotInAnyCidr_error() { + L3NetworkInventory destL3 = env.inventoryByName("flatL3_range_noDhcp") + L3NetworkInventory srcL3 = env.inventoryByName("flatL3_dest") + + VmInstanceInventory vm = createVmOnL3("vm-resolve-b3", srcL3.uuid) + VmNicInventory vmNic = vm.vmNics[0] + + expect(AssertionError.class) { + changeVmNicNetwork { + vmNicUuid = vmNic.uuid + destL3NetworkUuid = destL3.uuid + systemTags = [ + String.format("staticIp::%s::10.0.0.50", destL3.uuid), + String.format("ipv4Gateway::%s::10.0.0.1", destL3.uuid) + ] + } + } + } + + /** + * c-1: netmask provided (no gateway), netmask == CIDR netmask. + * Expected: success, gateway from CIDR. + */ + void testResolve_C1_netmaskMatchesCidr_success() { + L3NetworkInventory destL3 = env.inventoryByName("flatL3_range_noDhcp") + L3NetworkInventory srcL3 = env.inventoryByName("flatL3_dest") + + VmInstanceInventory vm = createVmOnL3("vm-resolve-c1", srcL3.uuid) + VmNicInventory vmNic = vm.vmNics[0] + + changeVmNicNetwork { + vmNicUuid = vmNic.uuid + destL3NetworkUuid = destL3.uuid + systemTags = [ + String.format("staticIp::%s::192.168.100.60", destL3.uuid), + String.format("ipv4Netmask::%s::255.255.255.0", destL3.uuid) + ] + } + + VmNicVO nicVO = dbFindByUuid(vmNic.uuid, VmNicVO.class) + assert nicVO.ip == "192.168.100.60" + assert nicVO.netmask == "255.255.255.0" + assert nicVO.gateway == "192.168.100.1" : "gateway should be inferred from L3 CIDR" + } + + /** + * c-4: netmask != CIDR, non-default & non-sole. + * VM has 2 NICs [flatL3_dest(default), flatL3_second]. + * Change flatL3_second NIC → flatL3_range_noDhcp (not default, vmNicCount=2). + * Expected: success, netmask=user input, gateway="". + */ + void testResolve_C4_netmaskMismatch_nonDefaultNonSole_success() { + L3NetworkInventory destL3 = env.inventoryByName("flatL3_range_noDhcp") + L3NetworkInventory srcL3 = env.inventoryByName("flatL3_dest") + L3NetworkInventory secondL3 = env.inventoryByName("flatL3_second") + + VmInstanceInventory vm = createVmOnL3("vm-resolve-c4", [srcL3.uuid, secondL3.uuid], srcL3.uuid) + + int nicCount = Q.New(VmNicVO.class).eq(VmNicVO_.vmInstanceUuid, vm.uuid).count().intValue() + assert nicCount == 2 + + String defaultL3 = Q.New(VmInstanceVO.class) + .select(VmInstanceVO_.defaultL3NetworkUuid) + .eq(VmInstanceVO_.uuid, vm.uuid) + .findValue() + assert defaultL3 == srcL3.uuid + assert defaultL3 != destL3.uuid + + VmNicInventory nicOnSecond = vm.vmNics.find { it.l3NetworkUuid == secondL3.uuid } + assert nicOnSecond != null + + changeVmNicNetwork { + vmNicUuid = nicOnSecond.uuid + destL3NetworkUuid = destL3.uuid + systemTags = [ + String.format("staticIp::%s::192.168.100.90", destL3.uuid), + String.format("ipv4Netmask::%s::255.255.0.0", destL3.uuid) + ] + } + + VmNicVO nicVO = dbFindByUuid(nicOnSecond.uuid, VmNicVO.class) + assert nicVO.ip == "192.168.100.90" + assert nicVO.netmask == "255.255.0.0" : "netmask should be user input" + assert nicVO.gateway == "" || nicVO.gateway == null : "gateway should be empty" + } + + /** + * d-1: neither netmask nor gateway, IP in L3 CIDR. + * Expected: success, netmask+gateway from CIDR. + */ + void testResolve_D1_ipInCidr_success() { + L3NetworkInventory destL3 = env.inventoryByName("flatL3_range_noDhcp") + L3NetworkInventory srcL3 = env.inventoryByName("flatL3_dest") + + VmInstanceInventory vm = createVmOnL3("vm-resolve-d1", srcL3.uuid) + VmNicInventory vmNic = vm.vmNics[0] + + changeVmNicNetwork { + vmNicUuid = vmNic.uuid + destL3NetworkUuid = destL3.uuid + systemTags = [ + String.format("staticIp::%s::192.168.100.100", destL3.uuid) + ] + } + + VmNicVO nicVO = dbFindByUuid(vmNic.uuid, VmNicVO.class) + assert nicVO.ip == "192.168.100.100" + assert nicVO.netmask == "255.255.255.0" : "netmask should be inferred from L3 CIDR" + assert nicVO.gateway == "192.168.100.1" : "gateway should be inferred from L3 CIDR" + } + + /** + * d-2: neither netmask nor gateway, IP NOT in any CIDR. + * Expected: error. + */ + void testResolve_D2_ipOutsideCidr_error() { + L3NetworkInventory destL3 = env.inventoryByName("flatL3_range_noDhcp") + L3NetworkInventory srcL3 = env.inventoryByName("flatL3_dest") + + VmInstanceInventory vm = createVmOnL3("vm-resolve-d2", srcL3.uuid) + VmNicInventory vmNic = vm.vmNics[0] + + expect(AssertionError.class) { + changeVmNicNetwork { + vmNicUuid = vmNic.uuid + destL3NetworkUuid = destL3.uuid + systemTags = [ + String.format("staticIp::%s::10.0.0.100", destL3.uuid) + ] + } + } + } + + /** + * d-3 (setVmStaticIp only): existing IP has params, new IP in old CIDR → reuse. + * This scenario is unique to APISetVmStaticIpMsg (existing IP reuse via ExistingIpContext). + */ + void testResolve_D3_setStaticIp_existingIpReuse_success() { + L3NetworkInventory l3 = env.inventoryByName("flatL3_range_noDhcp") + + VmInstanceInventory vm = createVmOnL3("vm-resolve-d3", l3.uuid) + + // First set a known IP with explicit netmask/gateway + setVmStaticIp { + vmInstanceUuid = vm.uuid + l3NetworkUuid = l3.uuid + ip = "192.168.100.110" + netmask = "255.255.255.0" + gateway = "192.168.100.1" + } + + // Now change to a new IP in the same CIDR, without specifying netmask/gateway + setVmStaticIp { + vmInstanceUuid = vm.uuid + l3NetworkUuid = l3.uuid + ip = "192.168.100.111" + } + + UsedIpVO usedIp = Q.New(UsedIpVO.class) + .eq(UsedIpVO_.l3NetworkUuid, l3.uuid) + .eq(UsedIpVO_.ip, "192.168.100.111") + .find() + assert usedIp != null + assert usedIp.netmask == "255.255.255.0" : "should reuse old netmask" + assert usedIp.gateway == "192.168.100.1" : "should reuse old gateway" + } +} diff --git a/test/src/test/groovy/org/zstack/test/integration/networkservice/provider/flat/PublicNetworkChangeVmIpOutsideCidrCase.groovy b/test/src/test/groovy/org/zstack/test/integration/networkservice/provider/flat/PublicNetworkChangeVmIpOutsideCidrCase.groovy new file mode 100644 index 00000000000..3b1ae524137 --- /dev/null +++ b/test/src/test/groovy/org/zstack/test/integration/networkservice/provider/flat/PublicNetworkChangeVmIpOutsideCidrCase.groovy @@ -0,0 +1,398 @@ +package org.zstack.test.integration.networkservice.provider.flat + +import org.springframework.http.HttpEntity +import org.zstack.core.db.DatabaseFacade +import org.zstack.core.db.Q +import org.zstack.header.network.l3.UsedIpVO +import org.zstack.header.network.l3.UsedIpVO_ +import org.zstack.header.network.service.NetworkServiceType +import org.zstack.header.vm.VmNicVO +import org.zstack.network.securitygroup.SecurityGroupConstant +import org.zstack.network.service.eip.EipConstant +import org.zstack.network.service.flat.FlatDhcpBackend +import org.zstack.network.service.flat.FlatNetworkServiceConstant +import org.zstack.sdk.* +import org.zstack.test.integration.networkservice.provider.NetworkServiceProviderTest +import org.zstack.testlib.EnvSpec +import org.zstack.testlib.SubCase +import org.zstack.utils.data.SizeUnit +import org.zstack.utils.gson.JSONObjectUtil + +/** + * Test IP outside CIDR behavior for public networks. + * Outside-range IPs are always allowed (no global config needed). + * + * Public network combinations (2 combos): + * 1. pubL3_range_noDhcp — has IP range, no DHCP (enableIPAM=true, enableIpAddressAllocation()=false) + * 2. pubL3_range_dhcp — has IP range, has DHCP (enableIPAM=true, enableIpAddressAllocation()=true) + * + * Each scenario tests: setVmStaticIp, changeVmNicNetwork, DHCP skip. + * No EIP tests — public networks do not need EIP testing. + */ +class PublicNetworkChangeVmIpOutsideCidrCase extends SubCase { + + EnvSpec env + DatabaseFacade dbf + + @Override + void setup() { + useSpring(NetworkServiceProviderTest.springSpec) + } + + @Override + void clean() { + env.delete() + } + + @Override + void environment() { + env = env { + instanceOffering { + name = "instanceOffering" + memory = SizeUnit.GIGABYTE.toByte(1) + cpu = 1 + } + + sftpBackupStorage { + name = "sftp" + url = "/sftp" + username = "root" + password = "password" + hostname = "localhost" + + image { + name = "image1" + url = "http://zstack.org/download/test.qcow2" + } + } + + zone { + name = "zone" + description = "test" + + cluster { + name = "cluster" + hypervisorType = "KVM" + + kvm { + name = "kvm" + managementIp = "127.0.0.1" + username = "root" + password = "password" + } + + attachPrimaryStorage("local") + attachL2Network("l2-pub-range-noDhcp") + attachL2Network("l2-pub-range-dhcp") + attachL2Network("l2-dest") + } + + localPrimaryStorage { + name = "local" + url = "/local_ps" + } + + // ========== Public: has IP range, no DHCP ========== + l2NoVlanNetwork { + name = "l2-pub-range-noDhcp" + physicalInterface = "eth0" + + l3Network { + name = "pubL3_range_noDhcp" + category = "Public" + + service { + provider = SecurityGroupConstant.SECURITY_GROUP_PROVIDER_TYPE + types = [SecurityGroupConstant.SECURITY_GROUP_NETWORK_SERVICE_TYPE] + } + + ip { + startIp = "12.100.10.10" + endIp = "12.100.10.200" + netmask = "255.255.255.0" + gateway = "12.100.10.1" + } + } + } + + // ========== Public: has IP range, has DHCP ========== + l2NoVlanNetwork { + name = "l2-pub-range-dhcp" + physicalInterface = "eth1" + + l3Network { + name = "pubL3_range_dhcp" + category = "Public" + + service { + provider = FlatNetworkServiceConstant.FLAT_NETWORK_SERVICE_TYPE_STRING + types = [NetworkServiceType.DHCP.toString(), + EipConstant.EIP_NETWORK_SERVICE_TYPE] + } + service { + provider = SecurityGroupConstant.SECURITY_GROUP_PROVIDER_TYPE + types = [SecurityGroupConstant.SECURITY_GROUP_NETWORK_SERVICE_TYPE] + } + + ip { + startIp = "12.100.20.10" + endIp = "12.100.20.200" + netmask = "255.255.255.0" + gateway = "12.100.20.1" + } + } + } + + // ========== Destination L3 for changeVmNicNetwork tests ========== + l2NoVlanNetwork { + name = "l2-dest" + physicalInterface = "eth2" + + l3Network { + name = "flatL3_dest" + enableIPAM = false + + service { + provider = SecurityGroupConstant.SECURITY_GROUP_PROVIDER_TYPE + types = [SecurityGroupConstant.SECURITY_GROUP_NETWORK_SERVICE_TYPE] + } + // No IP range, no DHCP — used as changeVmNicNetwork source + } + } + + attachBackupStorage("sftp") + } + } + } + + @Override + void test() { + dbf = bean(DatabaseFacade.class) + env.create { + // ========================================== + // Outside-range IPs are always allowed (no global config needed) + // ========================================== + + // --- Public: has IP range, no DHCP --- + testSetStaticIp_pubRangeNoDhcp() + testChangeNicNetwork_pubRangeNoDhcp() + testDhcpSkip_pubRangeNoDhcp() + + // --- Public: has IP range, has DHCP --- + testSetStaticIp_pubRangeDhcp() + testChangeNicNetwork_pubRangeDhcp() + testDhcpSkip_pubRangeDhcp() + } + } + + // ================================================================ + // Helper: create a VM on a given L3 + // ================================================================ + + VmInstanceInventory createVmOnL3(String vmName, String l3Uuid) { + return createVmInstance { + name = vmName + imageUuid = env.inventoryByName("image1").uuid + instanceOfferingUuid = env.inventoryByName("instanceOffering").uuid + l3NetworkUuids = [l3Uuid] + } + } + + // ================================================================ + // Public: has IP range, no DHCP + // ================================================================ + + /** + * Public/range/no-DHCP: outside-range IP should succeed; in-range IP also works. + */ + void testSetStaticIp_pubRangeNoDhcp() { + L3NetworkInventory l3 = env.inventoryByName("pubL3_range_noDhcp") + + VmInstanceInventory vm = createVmOnL3("vm-pub-range-noDhcp-set", l3.uuid) + + setVmStaticIp { + vmInstanceUuid = vm.uuid + l3NetworkUuid = l3.uuid + ip = "10.0.2.50" + netmask = "255.255.255.0" + gateway = "10.0.2.1" + } + + UsedIpVO usedIp = Q.New(UsedIpVO.class) + .eq(UsedIpVO_.vmNicUuid, vm.vmNics[0].uuid) + .eq(UsedIpVO_.ip, "10.0.2.50") + .find() + assert usedIp != null + assert usedIp.ipRangeUuid == null : "ipRangeUuid should be null for outside-range IP" + assert usedIp.netmask == "255.255.255.0" + assert usedIp.gateway == "10.0.2.1" + + VmNicVO nicVO = dbFindByUuid(vm.vmNics[0].uuid, VmNicVO.class) + assert nicVO.ip == "10.0.2.50" + } + + /** + * Public/range/no-DHCP: changeVmNicNetwork with outside-range IP should succeed. + */ + void testChangeNicNetwork_pubRangeNoDhcp() { + L3NetworkInventory l3 = env.inventoryByName("pubL3_range_noDhcp") + L3NetworkInventory srcL3 = env.inventoryByName("flatL3_dest") + + VmInstanceInventory vm = createVmOnL3("vm-pub-range-noDhcp-change", srcL3.uuid) + VmNicInventory vmNic = vm.vmNics[0] + + changeVmNicNetwork { + vmNicUuid = vmNic.uuid + destL3NetworkUuid = l3.uuid + systemTags = [ + String.format("staticIp::%s::10.0.2.60", l3.uuid), + String.format("ipv4Netmask::%s::255.255.255.0", l3.uuid), + String.format("ipv4Gateway::%s::10.0.2.1", l3.uuid) + ] + } + + VmNicVO nicVO = dbFindByUuid(vmNic.uuid, VmNicVO.class) + assert nicVO.l3NetworkUuid == l3.uuid + assert nicVO.ip == "10.0.2.60" + + UsedIpVO usedIp = Q.New(UsedIpVO.class) + .eq(UsedIpVO_.vmNicUuid, vmNic.uuid) + .eq(UsedIpVO_.ip, "10.0.2.60") + .find() + assert usedIp != null + assert usedIp.ipRangeUuid == null : "ipRangeUuid should be null for outside-range IP" + } + + /** + * Public/range/no-DHCP: outside-range IP should NOT appear in DHCP messages on reboot. + */ + void testDhcpSkip_pubRangeNoDhcp() { + VmInstanceInventory vm = queryVmInstance { conditions = ["name=vm-pub-range-noDhcp-set"] }[0] + + stopVmInstance { uuid = vm.uuid } + + boolean dhcpApplied = false + env.afterSimulator(FlatDhcpBackend.APPLY_DHCP_PATH) { rsp, HttpEntity e -> + FlatDhcpBackend.ApplyDhcpCmd cmd = JSONObjectUtil.toObject(e.body, FlatDhcpBackend.ApplyDhcpCmd.class) + for (def dhcp : cmd.dhcp) { + if (dhcp.ip == "10.0.2.50") { dhcpApplied = true } + } + return rsp + } + env.afterSimulator(FlatDhcpBackend.BATCH_APPLY_DHCP_PATH) { rsp, HttpEntity e -> + FlatDhcpBackend.BatchApplyDhcpCmd cmd = JSONObjectUtil.toObject(e.body, FlatDhcpBackend.BatchApplyDhcpCmd.class) + for (def dhcpInfo : cmd.dhcpInfos) { + for (def dhcp : dhcpInfo.dhcp) { + if (dhcp.ip == "10.0.2.50") { dhcpApplied = true } + } + } + return rsp + } + + startVmInstance { uuid = vm.uuid } + + assert !dhcpApplied : "DHCP should NOT include outside-range IP 10.0.2.50" + } + + // ================================================================ + // Part 2: Global config ON — Public: has IP range, has DHCP + // ================================================================ + + /** + * Public/range/DHCP: outside-range IP should succeed; in-range IP also works. + */ + void testSetStaticIp_pubRangeDhcp() { + L3NetworkInventory l3 = env.inventoryByName("pubL3_range_dhcp") + + VmInstanceInventory vm = createVmOnL3("vm-pub-range-dhcp-set", l3.uuid) + + setVmStaticIp { + vmInstanceUuid = vm.uuid + l3NetworkUuid = l3.uuid + ip = "10.0.3.50" + netmask = "255.255.255.0" + gateway = "10.0.3.1" + } + + UsedIpVO usedIp = Q.New(UsedIpVO.class) + .eq(UsedIpVO_.vmNicUuid, vm.vmNics[0].uuid) + .eq(UsedIpVO_.ip, "10.0.3.50") + .find() + assert usedIp != null + assert usedIp.ipRangeUuid == null : "ipRangeUuid should be null for outside-range IP" + assert usedIp.netmask == "255.255.255.0" + assert usedIp.gateway == "10.0.3.1" + + VmNicVO nicVO = dbFindByUuid(vm.vmNics[0].uuid, VmNicVO.class) + assert nicVO.ip == "10.0.3.50" + } + + /** + * Public/range/DHCP: changeVmNicNetwork with outside-range IP should succeed. + */ + void testChangeNicNetwork_pubRangeDhcp() { + L3NetworkInventory l3 = env.inventoryByName("pubL3_range_dhcp") + L3NetworkInventory srcL3 = env.inventoryByName("flatL3_dest") + + VmInstanceInventory vm = createVmOnL3("vm-pub-range-dhcp-change", srcL3.uuid) + VmNicInventory vmNic = vm.vmNics[0] + + changeVmNicNetwork { + vmNicUuid = vmNic.uuid + destL3NetworkUuid = l3.uuid + systemTags = [ + String.format("staticIp::%s::10.0.3.60", l3.uuid), + String.format("ipv4Netmask::%s::255.255.255.0", l3.uuid), + String.format("ipv4Gateway::%s::10.0.3.1", l3.uuid) + ] + } + + VmNicVO nicVO = dbFindByUuid(vmNic.uuid, VmNicVO.class) + assert nicVO.l3NetworkUuid == l3.uuid + assert nicVO.ip == "10.0.3.60" + + UsedIpVO usedIp = Q.New(UsedIpVO.class) + .eq(UsedIpVO_.vmNicUuid, vmNic.uuid) + .eq(UsedIpVO_.ip, "10.0.3.60") + .find() + assert usedIp != null + assert usedIp.ipRangeUuid == null : "ipRangeUuid should be null for outside-range IP" + } + + /** + * Public/range/DHCP: outside-range IP should NOT appear in DHCP messages on reboot, + * even though DHCP service is enabled on this L3. + */ + void testDhcpSkip_pubRangeDhcp() { + VmInstanceInventory vm = queryVmInstance { conditions = ["name=vm-pub-range-dhcp-set"] }[0] + + stopVmInstance { uuid = vm.uuid } + + boolean dhcpAppliedForOutsideIp = false + boolean dhcpTriggered = false + env.afterSimulator(FlatDhcpBackend.APPLY_DHCP_PATH) { rsp, HttpEntity e -> + dhcpTriggered = true + FlatDhcpBackend.ApplyDhcpCmd cmd = JSONObjectUtil.toObject(e.body, FlatDhcpBackend.ApplyDhcpCmd.class) + for (def dhcp : cmd.dhcp) { + if (dhcp.ip == "10.0.3.50") { dhcpAppliedForOutsideIp = true } + } + return rsp + } + env.afterSimulator(FlatDhcpBackend.BATCH_APPLY_DHCP_PATH) { rsp, HttpEntity e -> + dhcpTriggered = true + FlatDhcpBackend.BatchApplyDhcpCmd cmd = JSONObjectUtil.toObject(e.body, FlatDhcpBackend.BatchApplyDhcpCmd.class) + for (def dhcpInfo : cmd.dhcpInfos) { + for (def dhcp : dhcpInfo.dhcp) { + if (dhcp.ip == "10.0.3.50") { dhcpAppliedForOutsideIp = true } + } + } + return rsp + } + + startVmInstance { uuid = vm.uuid } + + assert !dhcpTriggered : "expected DHCP backend to run on a DHCP-enabled public L3" + assert !dhcpAppliedForOutsideIp : "DHCP should NOT include outside-range IP 10.0.3.50 even on DHCP-enabled public L3" + } +} + diff --git a/test/src/test/groovy/org/zstack/test/integration/networkservice/provider/flat/dhcp/VerifyPrepareDhcpWhenReconnectHostCase.groovy b/test/src/test/groovy/org/zstack/test/integration/networkservice/provider/flat/dhcp/VerifyPrepareDhcpWhenReconnectHostCase.groovy index 6e7d69c10eb..f3b9239e879 100644 --- a/test/src/test/groovy/org/zstack/test/integration/networkservice/provider/flat/dhcp/VerifyPrepareDhcpWhenReconnectHostCase.groovy +++ b/test/src/test/groovy/org/zstack/test/integration/networkservice/provider/flat/dhcp/VerifyPrepareDhcpWhenReconnectHostCase.groovy @@ -1,6 +1,7 @@ package org.zstack.test.integration.networkservice.provider.flat.dhcp import org.springframework.http.HttpEntity +import org.zstack.core.thread.ThreadFacade import org.zstack.header.network.service.NetworkServiceType import org.zstack.network.securitygroup.SecurityGroupConstant import org.zstack.network.service.eip.EipConstant @@ -9,6 +10,9 @@ import org.zstack.network.service.flat.FlatNetworkServiceConstant import org.zstack.network.service.userdata.UserdataConstant import org.zstack.network.service.virtualrouter.vyos.VyosConstants import org.zstack.sdk.HostInventory +import org.zstack.sdk.ImageInventory +import org.zstack.sdk.InstanceOfferingInventory +import org.zstack.sdk.L3NetworkInventory import org.zstack.sdk.VirtualRouterVmInventory import org.zstack.sdk.VmInstanceInventory import org.zstack.test.integration.networkservice.provider.NetworkServiceProviderTest @@ -17,6 +21,10 @@ import org.zstack.testlib.SubCase import org.zstack.utils.data.SizeUnit import org.zstack.utils.gson.JSONObjectUtil +import java.util.concurrent.CountDownLatch +import java.util.concurrent.TimeUnit +import java.util.concurrent.atomic.AtomicInteger + class VerifyPrepareDhcpWhenReconnectHostCase extends SubCase { EnvSpec env @Override @@ -147,12 +155,14 @@ class VerifyPrepareDhcpWhenReconnectHostCase extends SubCase { void test() { env.create { checkDhcpWork() + testBatchStartVmApplyDhcp() } } void checkDhcpWork(){ def host = queryHost {}[0] as HostInventory def vm = env.inventoryByName("vm") as VmInstanceInventory + def vmItemTokens = new LinkedHashSet() setVmHostname { uuid = vm.uuid @@ -164,6 +174,11 @@ class VerifyPrepareDhcpWhenReconnectHostCase extends SubCase { env.afterSimulator(FlatDhcpBackend.BATCH_APPLY_DHCP_PATH) { rsp, HttpEntity e1 -> cmd = JSONObjectUtil.toObject(e1.body, FlatDhcpBackend.BatchApplyDhcpCmd.class) called += 1 + cmd.dhcpInfos.each { info -> + info.dhcp.each { dhcp -> + vmItemTokens.add(String.format("%s-%s-%s", dhcp.ip, dhcp.netmask, dhcp.gateway)) + } + } return rsp } @@ -176,12 +191,17 @@ class VerifyPrepareDhcpWhenReconnectHostCase extends SubCase { assert called == 1 assert cmd.dhcpInfos.size() == 1 assert cmd.dhcpInfos.get(0).dhcp.get(0).hostname == "test-name" + def vmNic = vm.vmNics.get(0) + def expectedToken = String.format("%s-%s-%s", vmNic.ip, vmNic.netmask, vmNic.gateway) + assert vmItemTokens.contains(expectedToken) called = 0 cmd = null + vmItemTokens.clear() reconnectHost { uuid=host.uuid } assert called == 1 assert cmd.dhcpInfos.get(0).dhcp.get(0).hostname == "test-name" + assert vmItemTokens.contains(expectedToken) def vr = queryVirtualRouterVm {}[0] as VirtualRouterVmInventory assert vr != null @@ -192,6 +212,129 @@ class VerifyPrepareDhcpWhenReconnectHostCase extends SubCase { assert called == 1 } + void testBatchStartVmApplyDhcp() { + L3NetworkInventory l3 = env.inventoryByName("l3-1") as L3NetworkInventory + ImageInventory image = env.inventoryByName("image") as ImageInventory + InstanceOfferingInventory offering = env.inventoryByName("instanceOffering") as InstanceOfferingInventory + + def vmCount = 4 + def vms = new ArrayList() + def hostnameByIp = new LinkedHashMap() + (0.. + def hname = "batch-${idx}" + VmInstanceInventory inv = createVmInstance { + name = "batch-vm-${idx}" + imageUuid = image.uuid + l3NetworkUuids = [l3.uuid] + instanceOfferingUuid = offering.uuid + } as VmInstanceInventory + setVmHostname { + uuid = inv.uuid + hostname = hname + } + hostnameByIp.put(inv.vmNics.get(0).ip, hname) + vms.add(inv) + } + + vms.each { vmInv -> + stopVmInstance { + uuid = vmInv.uuid + } + } + + def batchCmds = Collections.synchronizedList(new ArrayList()) + def firstBatchArrived = new CountDownLatch(1) + def releaseFirstBatch = new CountDownLatch(1) + env.afterSimulator(FlatDhcpBackend.BATCH_APPLY_DHCP_PATH) { rsp, HttpEntity e1 -> + FlatDhcpBackend.BatchApplyDhcpCmd cmd = JSONObjectUtil.toObject(e1.body, FlatDhcpBackend.BatchApplyDhcpCmd.class) + batchCmds.add(cmd) + if (batchCmds.size() == 1) { + firstBatchArrived.countDown() + releaseFirstBatch.await(10, TimeUnit.SECONDS) + } + return rsp + } + + VmInstanceInventory blocker = vms.remove(0) + new Thread({ + startVmInstance { + uuid = blocker.uuid + } + }).start() + assert firstBatchArrived.await(10, TimeUnit.SECONDS) + + CountDownLatch doneLatch = new CountDownLatch(vms.size()) + vms.each { vmInv -> + new Thread({ + try { + startVmInstance { + uuid = vmInv.uuid + } + } finally { + doneLatch.countDown() + } + }).start() + } + + ThreadFacade thdf = bean(ThreadFacade.class) + retryInSecs { + assert thdf.getChainTaskInfo(String.format("coalesce-queue-flat-dhcp-apply-%s", vms[0].hostUuid)).pendingTask.size() == 3 + } + + releaseFirstBatch.countDown() + assert doneLatch.await(2, TimeUnit.MINUTES) + + retryInSecs(5) { + assert batchCmds.size() == 2 + } + retryInSecs(2) { + assert batchCmds.size() == 2 + } + + Closure> toTokenSet = { FlatDhcpBackend.BatchApplyDhcpCmd batch -> + def tokens = new LinkedHashSet() + batch.dhcpInfos.each { info -> + info.dhcp.each { dhcp -> + tokens.add(String.format("%s-%s-%s", dhcp.ip, dhcp.netmask, dhcp.gateway)) + } + } + return tokens + } + + Closure> toHostnameMap = { FlatDhcpBackend.BatchApplyDhcpCmd batch -> + def hostnames = new LinkedHashMap() + batch.dhcpInfos.each { info -> + info.dhcp.each { dhcp -> + hostnames.put(dhcp.ip, dhcp.hostname) + } + } + return hostnames + } + + def firstBatchTokens = toTokenSet(batchCmds.get(0)) + def secondBatchTokens = toTokenSet(batchCmds.get(1)) + def firstBatchHostnames = toHostnameMap(batchCmds.get(0)) + def secondBatchHostnames = toHostnameMap(batchCmds.get(1)) + + def blockerNic = blocker.vmNics.get(0) + def blockerToken = String.format("%s-%s-%s", blockerNic.ip, blockerNic.netmask, blockerNic.gateway) + assert firstBatchTokens.size() == 1 + assert firstBatchTokens.contains(blockerToken) + assert firstBatchHostnames.size() == 1 + assert firstBatchHostnames.get(blockerNic.ip) == hostnameByIp.get(blockerNic.ip) + + def expectedTokens = new LinkedHashSet() + def expectedHostnames = new LinkedHashMap() + vms.each { vmInv -> + def nic = vmInv.vmNics.get(0) + expectedTokens.add(String.format("%s-%s-%s", nic.ip, nic.netmask, nic.gateway)) + expectedHostnames.put(nic.ip, hostnameByIp.get(nic.ip)) + } + assert secondBatchTokens.containsAll(expectedTokens) + assert secondBatchTokens.size() == expectedTokens.size() + assert secondBatchHostnames == expectedHostnames + } + @Override void clean() { env.delete() diff --git a/test/src/test/groovy/org/zstack/test/integration/networkservice/provider/flat/securitygroup/AddSecurityGroupRuleOptimizedCase.groovy b/test/src/test/groovy/org/zstack/test/integration/networkservice/provider/flat/securitygroup/AddSecurityGroupRuleOptimizedCase.groovy index ed9a2736c5f..5160f0c2d9a 100644 --- a/test/src/test/groovy/org/zstack/test/integration/networkservice/provider/flat/securitygroup/AddSecurityGroupRuleOptimizedCase.groovy +++ b/test/src/test/groovy/org/zstack/test/integration/networkservice/provider/flat/securitygroup/AddSecurityGroupRuleOptimizedCase.groovy @@ -540,7 +540,7 @@ class AddSecurityGroupRuleOptimizedCase extends SubCase { addSecurityGroupRule { securityGroupUuid = sg3.uuid rules = [rule_82] - priority = 82 + priority = 101 } } } @@ -591,13 +591,12 @@ class AddSecurityGroupRuleOptimizedCase extends SubCase { rule_13.protocol = "ALL" rule_13.startPort = -1 rule_13.endPort = -1 - expect(AssertionError) { - addSecurityGroupRule { - securityGroupUuid = sg3.uuid - rules = [rule_13] - priority = 13 - } + sg3 = addSecurityGroupRule { + securityGroupUuid = sg3.uuid + rules = [rule_13] + priority = 13 } + assert sg3.rules.find { it.allowedCidr == rule_13.allowedCidr && it.priority == 13 } != null SecurityGroupRuleAO rule_12 = new SecurityGroupRuleAO() rule_12.dstIpRange = "2.2.2.2-2.2.2.10" @@ -609,12 +608,10 @@ class AddSecurityGroupRuleOptimizedCase extends SubCase { ingressRule.protocol = "TCP" ingressRule.dstPortRange = "12-13" - expect(AssertionError) { - addSecurityGroupRule { - securityGroupUuid = sg3.uuid - rules = [rule_12, ingressRule] - priority = 12 - } + sg3 = addSecurityGroupRule { + securityGroupUuid = sg3.uuid + rules = [rule_12, ingressRule] + priority = 12 } } diff --git a/test/src/test/groovy/org/zstack/test/integration/networkservice/provider/flat/securitygroup/ChangeSecurityGroupRuleCase.groovy b/test/src/test/groovy/org/zstack/test/integration/networkservice/provider/flat/securitygroup/ChangeSecurityGroupRuleCase.groovy index ad29787ef1e..844b1ab35d6 100644 --- a/test/src/test/groovy/org/zstack/test/integration/networkservice/provider/flat/securitygroup/ChangeSecurityGroupRuleCase.groovy +++ b/test/src/test/groovy/org/zstack/test/integration/networkservice/provider/flat/securitygroup/ChangeSecurityGroupRuleCase.groovy @@ -231,17 +231,15 @@ class ChangeSecurityGroupRuleCase extends SubCase { assert sg3 != null SecurityGroupRuleInventory rule_1 = sg3.rules.find { it.type == "Ingress" && it.priority == 1 && it.ipVersion == 4 } - expect(AssertionError) { - changeSecurityGroupRule { - uuid = rule_1.uuid - priority = 6 - } + changeSecurityGroupRule { + uuid = rule_1.uuid + priority = 6 } expect(AssertionError) { changeSecurityGroupRule { uuid = rule_1.uuid - priority = 7 + priority = 101 } } } @@ -307,11 +305,9 @@ class ChangeSecurityGroupRuleCase extends SubCase { } } - expect(AssertionError) { - changeSecurityGroupRule { - uuid = rule1.uuid - priority = 3 - } + changeSecurityGroupRule { + uuid = rule1.uuid + priority = 3 } expect(AssertionError) { diff --git a/test/src/test/groovy/org/zstack/test/unittest/identity/TestAPIQueryAccountMsgRBACCase.java b/test/src/test/groovy/org/zstack/test/unittest/identity/TestAPIQueryAccountMsgRBACCase.java new file mode 100644 index 00000000000..15216cb50bb --- /dev/null +++ b/test/src/test/groovy/org/zstack/test/unittest/identity/TestAPIQueryAccountMsgRBACCase.java @@ -0,0 +1,43 @@ +package org.zstack.test.unittest.identity; + +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; +import org.zstack.header.identity.APIQueryAccountMsg; +import org.zstack.header.identity.RBACInfo; +import org.zstack.header.identity.rbac.RBAC; + +public class TestAPIQueryAccountMsgRBACCase { + + @Before + public void setUp() { + RBAC.permissions.clear(); + } + + @Test + public void testAPIQueryAccountMsgIsAdminOnly() { + RBACInfo rbacInfo = new RBACInfo(); + rbacInfo.permissions(); + + boolean isAdminOnly = RBAC.isAdminOnlyAPI(APIQueryAccountMsg.class.getName()); + Assert.assertTrue( + "APIQueryAccountMsg should be admin-only to prevent privilege escalation, " + + "but it is currently accessible to normal IAM users via the wildcard normalAPIs pattern", + isAdminOnly + ); + } + + @Test + public void testAPIQueryAccountMsgNotInNormalAPIs() { + RBACInfo rbacInfo = new RBACInfo(); + rbacInfo.permissions(); + + String apiName = APIQueryAccountMsg.class.getName(); + boolean isInNormalOnly = RBAC.permissions.stream() + .anyMatch(p -> p.getNormalAPIs().contains(apiName) && !p.getAdminOnlyAPIs().contains(apiName)); + Assert.assertFalse( + "APIQueryAccountMsg should NOT be accessible as a normal API", + isInNormalOnly + ); + } +} diff --git a/test/src/test/java/org/zstack/test/core/errorcode/TestGlobalErrorCodeI18n.java b/test/src/test/java/org/zstack/test/core/errorcode/TestGlobalErrorCodeI18n.java index 729e841c6d2..917cf44cbc2 100644 --- a/test/src/test/java/org/zstack/test/core/errorcode/TestGlobalErrorCodeI18n.java +++ b/test/src/test/java/org/zstack/test/core/errorcode/TestGlobalErrorCodeI18n.java @@ -128,8 +128,27 @@ public void testLocalizeErrorCodeList() { @Test public void testNoGlobalErrorCode() { ErrorCode error = new ErrorCode("SYS.1000", "test error"); - // no globalErrorCode set + // no globalErrorCode set — message should fall back to description i18nService.localizeErrorCode(error, "zh_CN"); - Assert.assertNull("message should remain null", error.getMessage()); + Assert.assertEquals("message should fall back to description", + "test error", error.getMessage()); + } + + @Test + public void testMessageGuaranteeFallbackToDetails() { + ErrorCode error = new ErrorCode("SYS.1000", "System Error", "disk full on /dev/sda1"); + i18nService.localizeErrorCode(error, "en_US"); + Assert.assertEquals("message should fall back to details", + "disk full on /dev/sda1", error.getMessage()); + } + + @Test + public void testMessageNeverNull() { + ErrorCode error = new ErrorCode("SYS.1000", "System Error"); + error.setDetails(null); + i18nService.localizeErrorCode(error, "en_US"); + Assert.assertNotNull("message must never be null after localizeErrorCode", + error.getMessage()); + Assert.assertEquals("System Error", error.getMessage()); } } \ No newline at end of file diff --git a/testlib/pom.xml b/testlib/pom.xml index 4d1688012a6..d1927c6d4fd 100644 --- a/testlib/pom.xml +++ b/testlib/pom.xml @@ -247,6 +247,39 @@ + + org.codehaus.mojo + aspectj-maven-plugin + ${aspectj.plugin.version} + + + + compile + test-compile + + + + + ${project.java.version} + ${project.java.version} + ${project.java.version} + true + + + org.springframework + spring-aspects + + + org.zstack + core + + + org.zstack + header + + + + diff --git a/testlib/src/main/java/org/zstack/testlib/ApiHelper.groovy b/testlib/src/main/java/org/zstack/testlib/ApiHelper.groovy index f8470e35bd3..db88ce60bae 100644 --- a/testlib/src/main/java/org/zstack/testlib/ApiHelper.groovy +++ b/testlib/src/main/java/org/zstack/testlib/ApiHelper.groovy @@ -962,6 +962,33 @@ abstract class ApiHelper { } + def addBareMetal2DpuChassis(@DelegatesTo(strategy = Closure.OWNER_FIRST, value = org.zstack.sdk.AddBareMetal2DpuChassisAction.class) Closure c) { + def a = new org.zstack.sdk.AddBareMetal2DpuChassisAction() + a.sessionId = Test.currentEnvSpec?.session?.uuid + c.resolveStrategy = Closure.OWNER_FIRST + c.delegate = a + c() + + + if (System.getProperty("apipath") != null) { + if (a.apiId == null) { + a.apiId = Platform.uuid + } + + def tracker = new ApiPathTracker(a.apiId) + def out = errorOut(a.call()) + def path = tracker.getApiPath() + if (!path.isEmpty()) { + Test.apiPaths[a.class.name] = path.join(" --->\n") + } + + return out + } else { + return errorOut(a.call()) + } + } + + def addBareMetal2Gateway(@DelegatesTo(strategy = Closure.OWNER_FIRST, value = org.zstack.sdk.AddBareMetal2GatewayAction.class) Closure c) { def a = new org.zstack.sdk.AddBareMetal2GatewayAction() a.sessionId = Test.currentEnvSpec?.session?.uuid @@ -1421,6 +1448,33 @@ abstract class ApiHelper { } + def addExternalServiceConfiguration(@DelegatesTo(strategy = Closure.OWNER_FIRST, value = org.zstack.sdk.AddExternalServiceConfigurationAction.class) Closure c) { + def a = new org.zstack.sdk.AddExternalServiceConfigurationAction() + a.sessionId = Test.currentEnvSpec?.session?.uuid + c.resolveStrategy = Closure.OWNER_FIRST + c.delegate = a + c() + + + if (System.getProperty("apipath") != null) { + if (a.apiId == null) { + a.apiId = Platform.uuid + } + + def tracker = new ApiPathTracker(a.apiId) + def out = errorOut(a.call()) + def path = tracker.getApiPath() + if (!path.isEmpty()) { + Test.apiPaths[a.class.name] = path.join(" --->\n") + } + + return out + } else { + return errorOut(a.call()) + } + } + + def addFiSecSecurityMachine(@DelegatesTo(strategy = Closure.OWNER_FIRST, value = org.zstack.sdk.AddFiSecSecurityMachineAction.class) Closure c) { def a = new org.zstack.sdk.AddFiSecSecurityMachineAction() a.sessionId = Test.currentEnvSpec?.session?.uuid @@ -14489,6 +14543,33 @@ abstract class ApiHelper { } + def deleteExternalServiceConfiguration(@DelegatesTo(strategy = Closure.OWNER_FIRST, value = org.zstack.sdk.DeleteExternalServiceConfigurationAction.class) Closure c) { + def a = new org.zstack.sdk.DeleteExternalServiceConfigurationAction() + a.sessionId = Test.currentEnvSpec?.session?.uuid + c.resolveStrategy = Closure.OWNER_FIRST + c.delegate = a + c() + + + if (System.getProperty("apipath") != null) { + if (a.apiId == null) { + a.apiId = Platform.uuid + } + + def tracker = new ApiPathTracker(a.apiId) + def out = errorOut(a.call()) + def path = tracker.getApiPath() + if (!path.isEmpty()) { + Test.apiPaths[a.class.name] = path.join(" --->\n") + } + + return out + } else { + return errorOut(a.call()) + } + } + + def deleteFirewall(@DelegatesTo(strategy = Closure.OWNER_FIRST, value = org.zstack.sdk.DeleteFirewallAction.class) Closure c) { def a = new org.zstack.sdk.DeleteFirewallAction() a.sessionId = Test.currentEnvSpec?.session?.uuid @@ -18485,6 +18566,33 @@ abstract class ApiHelper { } + def detachDGpuFromVm(@DelegatesTo(strategy = Closure.OWNER_FIRST, value = org.zstack.sdk.DetachDGpuFromVmAction.class) Closure c) { + def a = new org.zstack.sdk.DetachDGpuFromVmAction() + a.sessionId = Test.currentEnvSpec?.session?.uuid + c.resolveStrategy = Closure.OWNER_FIRST + c.delegate = a + c() + + + if (System.getProperty("apipath") != null) { + if (a.apiId == null) { + a.apiId = Platform.uuid + } + + def tracker = new ApiPathTracker(a.apiId) + def out = errorOut(a.call()) + def path = tracker.getApiPath() + if (!path.isEmpty()) { + Test.apiPaths[a.class.name] = path.join(" --->\n") + } + + return out + } else { + return errorOut(a.call()) + } + } + + def detachDataVolumeFromHost(@DelegatesTo(strategy = Closure.OWNER_FIRST, value = org.zstack.sdk.DetachDataVolumeFromHostAction.class) Closure c) { def a = new org.zstack.sdk.DetachDataVolumeFromHostAction() a.sessionId = Test.currentEnvSpec?.session?.uuid @@ -19592,6 +19700,33 @@ abstract class ApiHelper { } + def disableDGpuMode(@DelegatesTo(strategy = Closure.OWNER_FIRST, value = org.zstack.sdk.DisableDGpuModeAction.class) Closure c) { + def a = new org.zstack.sdk.DisableDGpuModeAction() + a.sessionId = Test.currentEnvSpec?.session?.uuid + c.resolveStrategy = Closure.OWNER_FIRST + c.delegate = a + c() + + + if (System.getProperty("apipath") != null) { + if (a.apiId == null) { + a.apiId = Platform.uuid + } + + def tracker = new ApiPathTracker(a.apiId) + def out = errorOut(a.call()) + def path = tracker.getApiPath() + if (!path.isEmpty()) { + Test.apiPaths[a.class.name] = path.join(" --->\n") + } + + return out + } else { + return errorOut(a.call()) + } + } + + def discoverExternalPrimaryStorage(@DelegatesTo(strategy = Closure.OWNER_FIRST, value = org.zstack.sdk.DiscoverExternalPrimaryStorageAction.class) Closure c) { def a = new org.zstack.sdk.DiscoverExternalPrimaryStorageAction() a.sessionId = Test.currentEnvSpec?.session?.uuid @@ -19727,6 +19862,33 @@ abstract class ApiHelper { } + def enableDGpuMode(@DelegatesTo(strategy = Closure.OWNER_FIRST, value = org.zstack.sdk.EnableDGpuModeAction.class) Closure c) { + def a = new org.zstack.sdk.EnableDGpuModeAction() + a.sessionId = Test.currentEnvSpec?.session?.uuid + c.resolveStrategy = Closure.OWNER_FIRST + c.delegate = a + c() + + + if (System.getProperty("apipath") != null) { + if (a.apiId == null) { + a.apiId = Platform.uuid + } + + def tracker = new ApiPathTracker(a.apiId) + def out = errorOut(a.call()) + def path = tracker.getApiPath() + if (!path.isEmpty()) { + Test.apiPaths[a.class.name] = path.join(" --->\n") + } + + return out + } else { + return errorOut(a.call()) + } + } + + def executeAutoScalingRule(@DelegatesTo(strategy = Closure.OWNER_FIRST, value = org.zstack.sdk.ExecuteAutoScalingRuleAction.class) Closure c) { def a = new org.zstack.sdk.ExecuteAutoScalingRuleAction() a.sessionId = Test.currentEnvSpec?.session?.uuid @@ -21941,6 +22103,33 @@ abstract class ApiHelper { } + def getDGpuSpecStats(@DelegatesTo(strategy = Closure.OWNER_FIRST, value = org.zstack.sdk.GetDGpuSpecStatsAction.class) Closure c) { + def a = new org.zstack.sdk.GetDGpuSpecStatsAction() + a.sessionId = Test.currentEnvSpec?.session?.uuid + c.resolveStrategy = Closure.OWNER_FIRST + c.delegate = a + c() + + + if (System.getProperty("apipath") != null) { + if (a.apiId == null) { + a.apiId = Platform.uuid + } + + def tracker = new ApiPathTracker(a.apiId) + def out = errorOut(a.call()) + def path = tracker.getApiPath() + if (!path.isEmpty()) { + Test.apiPaths[a.class.name] = path.join(" --->\n") + } + + return out + } else { + return errorOut(a.call()) + } + } + + def getDataCenterFromRemote(@DelegatesTo(strategy = Closure.OWNER_FIRST, value = org.zstack.sdk.GetDataCenterFromRemoteAction.class) Closure c) { def a = new org.zstack.sdk.GetDataCenterFromRemoteAction() a.sessionId = Test.currentEnvSpec?.session?.uuid @@ -22400,6 +22589,33 @@ abstract class ApiHelper { } + def getGpuDeviceCandidates(@DelegatesTo(strategy = Closure.OWNER_FIRST, value = org.zstack.sdk.GetGpuDeviceCandidatesAction.class) Closure c) { + def a = new org.zstack.sdk.GetGpuDeviceCandidatesAction() + a.sessionId = Test.currentEnvSpec?.session?.uuid + c.resolveStrategy = Closure.OWNER_FIRST + c.delegate = a + c() + + + if (System.getProperty("apipath") != null) { + if (a.apiId == null) { + a.apiId = Platform.uuid + } + + def tracker = new ApiPathTracker(a.apiId) + def out = errorOut(a.call()) + def path = tracker.getApiPath() + if (!path.isEmpty()) { + Test.apiPaths[a.class.name] = path.join(" --->\n") + } + + return out + } else { + return errorOut(a.call()) + } + } + + def getGpuDeviceSpecCandidates(@DelegatesTo(strategy = Closure.OWNER_FIRST, value = org.zstack.sdk.GetGpuDeviceSpecCandidatesAction.class) Closure c) { def a = new org.zstack.sdk.GetGpuDeviceSpecCandidatesAction() a.sessionId = Test.currentEnvSpec?.session?.uuid @@ -26746,6 +26962,32 @@ abstract class ApiHelper { } } + def getVpcVRouterSnatLogState(@DelegatesTo(strategy = Closure.OWNER_FIRST, value = org.zstack.sdk.GetVpcVRouterSnatLogStateAction.class) Closure c) { + def a = new org.zstack.sdk.GetVpcVRouterSnatLogStateAction() + a.sessionId = Test.currentEnvSpec?.session?.uuid + c.resolveStrategy = Closure.OWNER_FIRST + c.delegate = a + c() + + + if (System.getProperty("apipath") != null) { + if (a.apiId == null) { + a.apiId = Platform.uuid + } + + def tracker = new ApiPathTracker(a.apiId) + def out = errorOut(a.call()) + def path = tracker.getApiPath() + if (!path.isEmpty()) { + Test.apiPaths[a.class.name] = path.join(" --->\n") + } + + return out + } else { + return errorOut(a.call()) + } + } + def getVpcVpnConfigurationFromRemote(@DelegatesTo(strategy = Closure.OWNER_FIRST, value = org.zstack.sdk.GetVpcVpnConfigurationFromRemoteAction.class) Closure c) { def a = new org.zstack.sdk.GetVpcVpnConfigurationFromRemoteAction() @@ -27503,6 +27745,33 @@ abstract class ApiHelper { } + def mountModelToVmInstance(@DelegatesTo(strategy = Closure.OWNER_FIRST, value = org.zstack.sdk.MountModelToVmInstanceAction.class) Closure c) { + def a = new org.zstack.sdk.MountModelToVmInstanceAction() + a.sessionId = Test.currentEnvSpec?.session?.uuid + c.resolveStrategy = Closure.OWNER_FIRST + c.delegate = a + c() + + + if (System.getProperty("apipath") != null) { + if (a.apiId == null) { + a.apiId = Platform.uuid + } + + def tracker = new ApiPathTracker(a.apiId) + def out = errorOut(a.call()) + def path = tracker.getApiPath() + if (!path.isEmpty()) { + Test.apiPaths[a.class.name] = path.join(" --->\n") + } + + return out + } else { + return errorOut(a.call()) + } + } + + def mountVmInstanceRecoveryPoint(@DelegatesTo(strategy = Closure.OWNER_FIRST, value = org.zstack.sdk.MountVmInstanceRecoveryPointAction.class) Closure c) { def a = new org.zstack.sdk.MountVmInstanceRecoveryPointAction() a.sessionId = Test.currentEnvSpec?.session?.uuid @@ -30198,8 +30467,8 @@ abstract class ApiHelper { } - def queryDRSAdvice(@DelegatesTo(strategy = Closure.OWNER_FIRST, value = org.zstack.sdk.QueryDRSAdviceAction.class) Closure c) { - def a = new org.zstack.sdk.QueryDRSAdviceAction() + def queryDGpuDevice(@DelegatesTo(strategy = Closure.OWNER_FIRST, value = org.zstack.sdk.QueryDGpuDeviceAction.class) Closure c) { + def a = new org.zstack.sdk.QueryDGpuDeviceAction() a.sessionId = Test.currentEnvSpec?.session?.uuid c.resolveStrategy = Closure.OWNER_FIRST c.delegate = a @@ -30227,8 +30496,8 @@ abstract class ApiHelper { } - def queryDRSVmMigrationActivity(@DelegatesTo(strategy = Closure.OWNER_FIRST, value = org.zstack.sdk.QueryDRSVmMigrationActivityAction.class) Closure c) { - def a = new org.zstack.sdk.QueryDRSVmMigrationActivityAction() + def queryDGpuProfile(@DelegatesTo(strategy = Closure.OWNER_FIRST, value = org.zstack.sdk.QueryDGpuProfileAction.class) Closure c) { + def a = new org.zstack.sdk.QueryDGpuProfileAction() a.sessionId = Test.currentEnvSpec?.session?.uuid c.resolveStrategy = Closure.OWNER_FIRST c.delegate = a @@ -30256,8 +30525,66 @@ abstract class ApiHelper { } - def queryDataCenterFromLocal(@DelegatesTo(strategy = Closure.OWNER_FIRST, value = org.zstack.sdk.QueryDataCenterFromLocalAction.class) Closure c) { - def a = new org.zstack.sdk.QueryDataCenterFromLocalAction() + def queryDRSAdvice(@DelegatesTo(strategy = Closure.OWNER_FIRST, value = org.zstack.sdk.QueryDRSAdviceAction.class) Closure c) { + def a = new org.zstack.sdk.QueryDRSAdviceAction() + a.sessionId = Test.currentEnvSpec?.session?.uuid + c.resolveStrategy = Closure.OWNER_FIRST + c.delegate = a + c() + + a.conditions = a.conditions.collect { it.toString() } + + + if (System.getProperty("apipath") != null) { + if (a.apiId == null) { + a.apiId = Platform.uuid + } + + def tracker = new ApiPathTracker(a.apiId) + def out = errorOut(a.call()) + def path = tracker.getApiPath() + if (!path.isEmpty()) { + Test.apiPaths[a.class.name] = path.join(" --->\n") + } + + return out + } else { + return errorOut(a.call()) + } + } + + + def queryDRSVmMigrationActivity(@DelegatesTo(strategy = Closure.OWNER_FIRST, value = org.zstack.sdk.QueryDRSVmMigrationActivityAction.class) Closure c) { + def a = new org.zstack.sdk.QueryDRSVmMigrationActivityAction() + a.sessionId = Test.currentEnvSpec?.session?.uuid + c.resolveStrategy = Closure.OWNER_FIRST + c.delegate = a + c() + + a.conditions = a.conditions.collect { it.toString() } + + + if (System.getProperty("apipath") != null) { + if (a.apiId == null) { + a.apiId = Platform.uuid + } + + def tracker = new ApiPathTracker(a.apiId) + def out = errorOut(a.call()) + def path = tracker.getApiPath() + if (!path.isEmpty()) { + Test.apiPaths[a.class.name] = path.join(" --->\n") + } + + return out + } else { + return errorOut(a.call()) + } + } + + + def queryDataCenterFromLocal(@DelegatesTo(strategy = Closure.OWNER_FIRST, value = org.zstack.sdk.QueryDataCenterFromLocalAction.class) Closure c) { + def a = new org.zstack.sdk.QueryDataCenterFromLocalAction() a.sessionId = Test.currentEnvSpec?.session?.uuid c.resolveStrategy = Closure.OWNER_FIRST c.delegate = a @@ -30778,6 +31105,35 @@ abstract class ApiHelper { } + def queryExternalServiceConfiguration(@DelegatesTo(strategy = Closure.OWNER_FIRST, value = org.zstack.sdk.QueryExternalServiceConfigurationAction.class) Closure c) { + def a = new org.zstack.sdk.QueryExternalServiceConfigurationAction() + a.sessionId = Test.currentEnvSpec?.session?.uuid + c.resolveStrategy = Closure.OWNER_FIRST + c.delegate = a + c() + + a.conditions = a.conditions.collect { it.toString() } + + + if (System.getProperty("apipath") != null) { + if (a.apiId == null) { + a.apiId = Platform.uuid + } + + def tracker = new ApiPathTracker(a.apiId) + def out = errorOut(a.call()) + def path = tracker.getApiPath() + if (!path.isEmpty()) { + Test.apiPaths[a.class.name] = path.join(" --->\n") + } + + return out + } else { + return errorOut(a.call()) + } + } + + def queryFaultToleranceVm(@DelegatesTo(strategy = Closure.OWNER_FIRST, value = org.zstack.sdk.QueryFaultToleranceVmAction.class) Closure c) { def a = new org.zstack.sdk.QueryFaultToleranceVmAction() a.sessionId = Test.currentEnvSpec?.session?.uuid @@ -35882,6 +36238,35 @@ abstract class ApiHelper { } + def queryVmModelMount(@DelegatesTo(strategy = Closure.OWNER_FIRST, value = org.zstack.sdk.QueryVmModelMountAction.class) Closure c) { + def a = new org.zstack.sdk.QueryVmModelMountAction() + a.sessionId = Test.currentEnvSpec?.session?.uuid + c.resolveStrategy = Closure.OWNER_FIRST + c.delegate = a + c() + + a.conditions = a.conditions.collect { it.toString() } + + + if (System.getProperty("apipath") != null) { + if (a.apiId == null) { + a.apiId = Platform.uuid + } + + def tracker = new ApiPathTracker(a.apiId) + def out = errorOut(a.call()) + def path = tracker.getApiPath() + if (!path.isEmpty()) { + Test.apiPaths[a.class.name] = path.join(" --->\n") + } + + return out + } else { + return errorOut(a.call()) + } + } + + def queryVmNic(@DelegatesTo(strategy = Closure.OWNER_FIRST, value = org.zstack.sdk.QueryVmNicAction.class) Closure c) { def a = new org.zstack.sdk.QueryVmNicAction() a.sessionId = Test.currentEnvSpec?.session?.uuid @@ -38565,6 +38950,33 @@ abstract class ApiHelper { } + def removeVmDGpuStrategy(@DelegatesTo(strategy = Closure.OWNER_FIRST, value = org.zstack.sdk.RemoveVmDGpuStrategyAction.class) Closure c) { + def a = new org.zstack.sdk.RemoveVmDGpuStrategyAction() + a.sessionId = Test.currentEnvSpec?.session?.uuid + c.resolveStrategy = Closure.OWNER_FIRST + c.delegate = a + c() + + + if (System.getProperty("apipath") != null) { + if (a.apiId == null) { + a.apiId = Platform.uuid + } + + def tracker = new ApiPathTracker(a.apiId) + def out = errorOut(a.call()) + def path = tracker.getApiPath() + if (!path.isEmpty()) { + Test.apiPaths[a.class.name] = path.join(" --->\n") + } + + return out + } else { + return errorOut(a.call()) + } + } + + def removeVmFromAffinityGroup(@DelegatesTo(strategy = Closure.OWNER_FIRST, value = org.zstack.sdk.RemoveVmFromAffinityGroupAction.class) Closure c) { def a = new org.zstack.sdk.RemoveVmFromAffinityGroupAction() a.sessionId = Test.currentEnvSpec?.session?.uuid @@ -39321,6 +39733,33 @@ abstract class ApiHelper { } + def setDGpuProfile(@DelegatesTo(strategy = Closure.OWNER_FIRST, value = org.zstack.sdk.SetDGpuProfileAction.class) Closure c) { + def a = new org.zstack.sdk.SetDGpuProfileAction() + a.sessionId = Test.currentEnvSpec?.session?.uuid + c.resolveStrategy = Closure.OWNER_FIRST + c.delegate = a + c() + + + if (System.getProperty("apipath") != null) { + if (a.apiId == null) { + a.apiId = Platform.uuid + } + + def tracker = new ApiPathTracker(a.apiId) + def out = errorOut(a.call()) + def path = tracker.getApiPath() + if (!path.isEmpty()) { + Test.apiPaths[a.class.name] = path.join(" --->\n") + } + + return out + } else { + return errorOut(a.call()) + } + } + + def setFlowMeterRouterId(@DelegatesTo(strategy = Closure.OWNER_FIRST, value = org.zstack.sdk.SetFlowMeterRouterIdAction.class) Closure c) { def a = new org.zstack.sdk.SetFlowMeterRouterIdAction() a.sessionId = Test.currentEnvSpec?.session?.uuid @@ -39915,6 +40354,33 @@ abstract class ApiHelper { } + def setVmDGpuStrategy(@DelegatesTo(strategy = Closure.OWNER_FIRST, value = org.zstack.sdk.SetVmDGpuStrategyAction.class) Closure c) { + def a = new org.zstack.sdk.SetVmDGpuStrategyAction() + a.sessionId = Test.currentEnvSpec?.session?.uuid + c.resolveStrategy = Closure.OWNER_FIRST + c.delegate = a + c() + + + if (System.getProperty("apipath") != null) { + if (a.apiId == null) { + a.apiId = Platform.uuid + } + + def tracker = new ApiPathTracker(a.apiId) + def out = errorOut(a.call()) + def path = tracker.getApiPath() + if (!path.isEmpty()) { + Test.apiPaths[a.class.name] = path.join(" --->\n") + } + + return out + } else { + return errorOut(a.call()) + } + } + + def setVmEmulatorPinning(@DelegatesTo(strategy = Closure.OWNER_FIRST, value = org.zstack.sdk.SetVmEmulatorPinningAction.class) Closure c) { def a = new org.zstack.sdk.SetVmEmulatorPinningAction() a.sessionId = Test.currentEnvSpec?.session?.uuid @@ -40508,6 +40974,32 @@ abstract class ApiHelper { } } + def setVpcVRouterSnatLogState(@DelegatesTo(strategy = Closure.OWNER_FIRST, value = org.zstack.sdk.SetVpcVRouterSnatLogStateAction.class) Closure c) { + def a = new org.zstack.sdk.SetVpcVRouterSnatLogStateAction() + a.sessionId = Test.currentEnvSpec?.session?.uuid + c.resolveStrategy = Closure.OWNER_FIRST + c.delegate = a + c() + + + if (System.getProperty("apipath") != null) { + if (a.apiId == null) { + a.apiId = Platform.uuid + } + + def tracker = new ApiPathTracker(a.apiId) + def out = errorOut(a.call()) + def path = tracker.getApiPath() + if (!path.isEmpty()) { + Test.apiPaths[a.class.name] = path.join(" --->\n") + } + + return out + } else { + return errorOut(a.call()) + } + } + def shareResource(@DelegatesTo(strategy = Closure.OWNER_FIRST, value = org.zstack.sdk.ShareResourceAction.class) Closure c) { def a = new org.zstack.sdk.ShareResourceAction() @@ -42237,6 +42729,33 @@ abstract class ApiHelper { } + def unmountModelFromVmInstance(@DelegatesTo(strategy = Closure.OWNER_FIRST, value = org.zstack.sdk.UnmountModelFromVmInstanceAction.class) Closure c) { + def a = new org.zstack.sdk.UnmountModelFromVmInstanceAction() + a.sessionId = Test.currentEnvSpec?.session?.uuid + c.resolveStrategy = Closure.OWNER_FIRST + c.delegate = a + c() + + + if (System.getProperty("apipath") != null) { + if (a.apiId == null) { + a.apiId = Platform.uuid + } + + def tracker = new ApiPathTracker(a.apiId) + def out = errorOut(a.call()) + def path = tracker.getApiPath() + if (!path.isEmpty()) { + Test.apiPaths[a.class.name] = path.join(" --->\n") + } + + return out + } else { + return errorOut(a.call()) + } + } + + def unmountVmInstanceRecoveryPoint(@DelegatesTo(strategy = Closure.OWNER_FIRST, value = org.zstack.sdk.UnmountVmInstanceRecoveryPointAction.class) Closure c) { def a = new org.zstack.sdk.UnmountVmInstanceRecoveryPointAction() a.sessionId = Test.currentEnvSpec?.session?.uuid @@ -44154,6 +44673,33 @@ abstract class ApiHelper { } + def updateExternalServiceConfiguration(@DelegatesTo(strategy = Closure.OWNER_FIRST, value = org.zstack.sdk.UpdateExternalServiceConfigurationAction.class) Closure c) { + def a = new org.zstack.sdk.UpdateExternalServiceConfigurationAction() + a.sessionId = Test.currentEnvSpec?.session?.uuid + c.resolveStrategy = Closure.OWNER_FIRST + c.delegate = a + c() + + + if (System.getProperty("apipath") != null) { + if (a.apiId == null) { + a.apiId = Platform.uuid + } + + def tracker = new ApiPathTracker(a.apiId) + def out = errorOut(a.call()) + def path = tracker.getApiPath() + if (!path.isEmpty()) { + Test.apiPaths[a.class.name] = path.join(" --->\n") + } + + return out + } else { + return errorOut(a.call()) + } + } + + def updateFactoryModeState(@DelegatesTo(strategy = Closure.OWNER_FIRST, value = org.zstack.sdk.UpdateFactoryModeStateAction.class) Closure c) { def a = new org.zstack.sdk.UpdateFactoryModeStateAction() a.sessionId = Test.currentEnvSpec?.session?.uuid @@ -47205,6 +47751,33 @@ abstract class ApiHelper { } + def updateVmInstancePciDeviceSpecRef(@DelegatesTo(strategy = Closure.OWNER_FIRST, value = org.zstack.sdk.UpdateVmInstancePciDeviceSpecRefAction.class) Closure c) { + def a = new org.zstack.sdk.UpdateVmInstancePciDeviceSpecRefAction() + a.sessionId = Test.currentEnvSpec?.session?.uuid + c.resolveStrategy = Closure.OWNER_FIRST + c.delegate = a + c() + + + if (System.getProperty("apipath") != null) { + if (a.apiId == null) { + a.apiId = Platform.uuid + } + + def tracker = new ApiPathTracker(a.apiId) + def out = errorOut(a.call()) + def path = tracker.getApiPath() + if (!path.isEmpty()) { + Test.apiPaths[a.class.name] = path.join(" --->\n") + } + + return out + } else { + return errorOut(a.call()) + } + } + + def updateVmNetworkConfig(@DelegatesTo(strategy = Closure.OWNER_FIRST, value = org.zstack.sdk.UpdateVmNetworkConfigAction.class) Closure c) { def a = new org.zstack.sdk.UpdateVmNetworkConfigAction() a.sessionId = Test.currentEnvSpec?.session?.uuid @@ -56650,6 +57223,116 @@ abstract class ApiHelper { } + def deleteResNotifySubscription(@DelegatesTo(strategy = Closure.OWNER_FIRST, value = org.zstack.sdk.zwatch.resnotify.DeleteResNotifySubscriptionAction.class) Closure c) { + def a = new org.zstack.sdk.zwatch.resnotify.DeleteResNotifySubscriptionAction() + a.sessionId = Test.currentEnvSpec?.session?.uuid + c.resolveStrategy = Closure.OWNER_FIRST + c.delegate = a + c() + + + if (System.getProperty("apipath") != null) { + if (a.apiId == null) { + a.apiId = Platform.uuid + } + + def tracker = new ApiPathTracker(a.apiId) + def out = errorOut(a.call()) + def path = tracker.getApiPath() + if (!path.isEmpty()) { + Test.apiPaths[a.class.name] = path.join(" --->\n") + } + + return out + } else { + return errorOut(a.call()) + } + } + + + def queryResNotifySubscription(@DelegatesTo(strategy = Closure.OWNER_FIRST, value = org.zstack.sdk.zwatch.resnotify.QueryResNotifySubscriptionAction.class) Closure c) { + def a = new org.zstack.sdk.zwatch.resnotify.QueryResNotifySubscriptionAction() + a.sessionId = Test.currentEnvSpec?.session?.uuid + c.resolveStrategy = Closure.OWNER_FIRST + c.delegate = a + c() + + a.conditions = a.conditions.collect { it.toString() } + + + if (System.getProperty("apipath") != null) { + if (a.apiId == null) { + a.apiId = Platform.uuid + } + + def tracker = new ApiPathTracker(a.apiId) + def out = errorOut(a.call()) + def path = tracker.getApiPath() + if (!path.isEmpty()) { + Test.apiPaths[a.class.name] = path.join(" --->\n") + } + + return out + } else { + return errorOut(a.call()) + } + } + + + def subscribeResNotify(@DelegatesTo(strategy = Closure.OWNER_FIRST, value = org.zstack.sdk.zwatch.resnotify.SubscribeResNotifyAction.class) Closure c) { + def a = new org.zstack.sdk.zwatch.resnotify.SubscribeResNotifyAction() + a.sessionId = Test.currentEnvSpec?.session?.uuid + c.resolveStrategy = Closure.OWNER_FIRST + c.delegate = a + c() + + + if (System.getProperty("apipath") != null) { + if (a.apiId == null) { + a.apiId = Platform.uuid + } + + def tracker = new ApiPathTracker(a.apiId) + def out = errorOut(a.call()) + def path = tracker.getApiPath() + if (!path.isEmpty()) { + Test.apiPaths[a.class.name] = path.join(" --->\n") + } + + return out + } else { + return errorOut(a.call()) + } + } + + + def updateResNotifySubscription(@DelegatesTo(strategy = Closure.OWNER_FIRST, value = org.zstack.sdk.zwatch.resnotify.UpdateResNotifySubscriptionAction.class) Closure c) { + def a = new org.zstack.sdk.zwatch.resnotify.UpdateResNotifySubscriptionAction() + a.sessionId = Test.currentEnvSpec?.session?.uuid + c.resolveStrategy = Closure.OWNER_FIRST + c.delegate = a + c() + + + if (System.getProperty("apipath") != null) { + if (a.apiId == null) { + a.apiId = Platform.uuid + } + + def tracker = new ApiPathTracker(a.apiId) + def out = errorOut(a.call()) + def path = tracker.getApiPath() + if (!path.isEmpty()) { + Test.apiPaths[a.class.name] = path.join(" --->\n") + } + + return out + } else { + return errorOut(a.call()) + } + } + + def addThirdpartyPlatform(@DelegatesTo(strategy = Closure.OWNER_FIRST, value = org.zstack.sdk.zwatch.thirdparty.api.AddThirdpartyPlatformAction.class) Closure c) { def a = new org.zstack.sdk.zwatch.thirdparty.api.AddThirdpartyPlatformAction() a.sessionId = Test.currentEnvSpec?.session?.uuid diff --git a/testlib/src/main/java/org/zstack/testlib/Test.groovy b/testlib/src/main/java/org/zstack/testlib/Test.groovy index dc3b369f8b2..40f46defad1 100755 --- a/testlib/src/main/java/org/zstack/testlib/Test.groovy +++ b/testlib/src/main/java/org/zstack/testlib/Test.groovy @@ -1001,6 +1001,20 @@ mysqldump -u root zstack > ${failureLogDir.absolutePath}/dbdump.sql } } + /** + * Expect an API call to fail and verify the error details. + * The second closure's delegate is the parsed {@link ErrorCodeList}, so you can + * directly access {@code code}, {@code details}, {@code globalErrorCode}, etc. + * + * Example: + *

+     *   expectApiFailure {
+     *       someApiCall { ... }
+     *   } {
+     *       assert details.contains("expected error keyword")
+     *   }
+     * 
+ */ static void expectApiFailure(Closure c, @DelegatesTo(strategy = Closure.OWNER_FIRST, value = ErrorCodeList.class) Closure errorCodeChecker) { AssertionError error = null diff --git a/testlib/src/main/java/org/zstack/testlib/core/FailCoalesceQueue.java b/testlib/src/main/java/org/zstack/testlib/core/FailCoalesceQueue.java new file mode 100644 index 00000000000..7627a34e1ca --- /dev/null +++ b/testlib/src/main/java/org/zstack/testlib/core/FailCoalesceQueue.java @@ -0,0 +1,22 @@ +package org.zstack.testlib.core; + +import org.zstack.core.thread.CoalesceQueue; +import org.zstack.header.core.Completion; +import org.zstack.header.errorcode.OperationFailureException; + +import java.util.List; + +import static org.zstack.core.Platform.operr; +import static org.zstack.utils.clouderrorcode.CloudOperationsErrorCode.ORG_ZSTACK_CORE_THREAD_10004; + +public class FailCoalesceQueue extends CoalesceQueue { + @Override + protected String getName() { + return "test-failure"; + } + + @Override + protected void executeBatch(List items, Completion completion) { + throw new OperationFailureException(operr(ORG_ZSTACK_CORE_THREAD_10004, "test error")); + } +} diff --git a/testlib/src/main/java/org/zstack/testlib/core/FailReturnValueCoalesceQueue.java b/testlib/src/main/java/org/zstack/testlib/core/FailReturnValueCoalesceQueue.java new file mode 100644 index 00000000000..2c99bd2ee3b --- /dev/null +++ b/testlib/src/main/java/org/zstack/testlib/core/FailReturnValueCoalesceQueue.java @@ -0,0 +1,27 @@ +package org.zstack.testlib.core; + +import org.zstack.core.thread.ReturnValueCoalesceQueue; +import org.zstack.header.core.ReturnValueCompletion; +import org.zstack.header.errorcode.OperationFailureException; + +import java.util.List; + +import static org.zstack.core.Platform.operr; +import static org.zstack.utils.clouderrorcode.CloudOperationsErrorCode.ORG_ZSTACK_CORE_THREAD_10004; + +public class FailReturnValueCoalesceQueue extends ReturnValueCoalesceQueue { + @Override + protected String getName() { + return "test-rv-failure"; + } + + @Override + protected void executeBatch(List items, ReturnValueCompletion completion) { + throw new OperationFailureException(operr(ORG_ZSTACK_CORE_THREAD_10004, "test rv error")); + } + + @Override + protected String calculateResult(Integer item, String batchResult) { + return null; + } +} diff --git a/testlib/src/main/java/org/zstack/testlib/core/ThrowOnFailCompletion.java b/testlib/src/main/java/org/zstack/testlib/core/ThrowOnFailCompletion.java new file mode 100644 index 00000000000..6ce51356b16 --- /dev/null +++ b/testlib/src/main/java/org/zstack/testlib/core/ThrowOnFailCompletion.java @@ -0,0 +1,31 @@ +package org.zstack.testlib.core; + +import org.zstack.header.core.Completion; +import org.zstack.header.errorcode.ErrorCode; + +import java.util.concurrent.CountDownLatch; + +/** + * A Completion that throws RuntimeException on fail(). + * Used to test that AspectJ weaving catches the exception + * and chain.next() still gets called. + */ +public class ThrowOnFailCompletion extends Completion { + private final CountDownLatch latch; + + public ThrowOnFailCompletion(CountDownLatch latch) { + super(null); + this.latch = latch; + } + + @Override + public void success() { + latch.countDown(); + } + + @Override + public void fail(ErrorCode errorCode) { + latch.countDown(); + throw new RuntimeException("intentional throw in fail()"); + } +} diff --git a/testlib/src/main/java/org/zstack/testlib/core/ThrowOnFailReturnValueCompletion.java b/testlib/src/main/java/org/zstack/testlib/core/ThrowOnFailReturnValueCompletion.java new file mode 100644 index 00000000000..25a1326b7dc --- /dev/null +++ b/testlib/src/main/java/org/zstack/testlib/core/ThrowOnFailReturnValueCompletion.java @@ -0,0 +1,31 @@ +package org.zstack.testlib.core; + +import org.zstack.header.core.ReturnValueCompletion; +import org.zstack.header.errorcode.ErrorCode; + +import java.util.concurrent.CountDownLatch; + +/** + * A ReturnValueCompletion that throws RuntimeException on fail(). + * Used to test that AspectJ weaving catches the exception + * and chain.next() still gets called. + */ +public class ThrowOnFailReturnValueCompletion extends ReturnValueCompletion { + private final CountDownLatch latch; + + public ThrowOnFailReturnValueCompletion(CountDownLatch latch) { + super(null); + this.latch = latch; + } + + @Override + public void success(String returnValue) { + latch.countDown(); + } + + @Override + public void fail(ErrorCode errorCode) { + latch.countDown(); + throw new RuntimeException("intentional throw in ReturnValueCompletion.fail()"); + } +} diff --git a/testlib/src/main/java/org/zstack/testlib/core/ThrowOnSuccessCompletion.java b/testlib/src/main/java/org/zstack/testlib/core/ThrowOnSuccessCompletion.java new file mode 100644 index 00000000000..5fa6ccdcdb6 --- /dev/null +++ b/testlib/src/main/java/org/zstack/testlib/core/ThrowOnSuccessCompletion.java @@ -0,0 +1,31 @@ +package org.zstack.testlib.core; + +import org.zstack.header.core.Completion; +import org.zstack.header.errorcode.ErrorCode; + +import java.util.concurrent.CountDownLatch; + +/** + * A Completion that throws RuntimeException on success(). + * Used to test that AspectJ weaving catches the exception + * and chain.next() still gets called. + */ +public class ThrowOnSuccessCompletion extends Completion { + private final CountDownLatch latch; + + public ThrowOnSuccessCompletion(CountDownLatch latch) { + super(null); + this.latch = latch; + } + + @Override + public void success() { + latch.countDown(); + throw new RuntimeException("intentional throw in success()"); + } + + @Override + public void fail(ErrorCode errorCode) { + latch.countDown(); + } +} diff --git a/testlib/src/main/java/org/zstack/testlib/core/ThrowOnSuccessReturnValueCompletion.java b/testlib/src/main/java/org/zstack/testlib/core/ThrowOnSuccessReturnValueCompletion.java new file mode 100644 index 00000000000..71cd5ed8a19 --- /dev/null +++ b/testlib/src/main/java/org/zstack/testlib/core/ThrowOnSuccessReturnValueCompletion.java @@ -0,0 +1,31 @@ +package org.zstack.testlib.core; + +import org.zstack.header.core.ReturnValueCompletion; +import org.zstack.header.errorcode.ErrorCode; + +import java.util.concurrent.CountDownLatch; + +/** + * A ReturnValueCompletion that throws RuntimeException on success(). + * Used to test that AspectJ weaving catches the exception + * and chain.next() still gets called. + */ +public class ThrowOnSuccessReturnValueCompletion extends ReturnValueCompletion { + private final CountDownLatch latch; + + public ThrowOnSuccessReturnValueCompletion(CountDownLatch latch) { + super(null); + this.latch = latch; + } + + @Override + public void success(String returnValue) { + latch.countDown(); + throw new RuntimeException("intentional throw in ReturnValueCompletion.success()"); + } + + @Override + public void fail(ErrorCode errorCode) { + latch.countDown(); + } +} diff --git a/testlib/src/main/resources/zssdk.py b/testlib/src/main/resources/zssdk.py index e82ddde0820..d8463cb54a8 100755 --- a/testlib/src/main/resources/zssdk.py +++ b/testlib/src/main/resources/zssdk.py @@ -4,7 +4,7 @@ try: import urllib3 except ImportError: - print 'urlib3 is not installed, run "pip install urlib3"' + print('urllib3 is not installed, run "pip install urllib3"') sys.exit(1) import string @@ -16,10 +16,13 @@ import traceback import base64 import hmac -import sha from hashlib import sha1 import datetime -import time + +try: + int_types = (int, long) +except NameError: + int_types = (int,) CONFIG_HOSTNAME = 'hostname' CONFIG_PORT = 'port' @@ -54,8 +57,8 @@ def _exception_safe(func): def wrap(*args, **kwargs): try: func(*args, **kwargs) - except: - print traceback.format_exc() + except Exception: + print(traceback.format_exc()) return wrap @@ -76,6 +79,7 @@ def _http_error(status, body=None): def _error(code, desc, details): err = ErrorCode() err.code = code + err.description = desc err.desc = desc err.details = details return {'error': err} @@ -177,7 +181,7 @@ def _check_params(self): if value is not None and isinstance(value, str) and annotation.max_length and len(value) > annotation.max_length: raise SdkError('invalid length[%s] of the parameter[%s], the max allowed length is %s' % (len(value), param_name, annotation.max_length)) - if value is not None and isinstance(value, str) and annotation.min_length and len(value) > annotation.min_length: + if value is not None and isinstance(value, str) and annotation.min_length and len(value) < annotation.min_length: raise SdkError('invalid length[%s] of the parameter[%s], the minimal allowed length is %s' % (len(value), param_name, annotation.min_length)) if value is not None and isinstance(value, list) and annotation.non_empty is True and len(value) == 0: @@ -189,7 +193,7 @@ def _check_params(self): if value is not None and isinstance(value, str) and annotation.empty_string is False and len(value) == 0: raise SdkError('invalid parameter[%s], it cannot be an empty string' % param_name) - if value is not None and (isinstance(value, int) or isinstance(value, long)) \ + if value is not None and isinstance(value, int_types) \ and annotation.number_range is not None and len(annotation.number_range) == 2: low = annotation.number_range[0] high = annotation.number_range[1] @@ -231,11 +235,11 @@ def _url(self): elements.append('/v1') path = self.PATH.replace('{', '${') - unresolved = re.findall('${(.+?)}', path) + unresolved = re.findall(r'\$\{(.+?)\}', path) params = self._params() if unresolved: for u in unresolved: - if u in params: + if u not in params: raise SdkError('missing a mandatory parameter[%s]' % u) path = string.Template(path).substitute(params) @@ -253,10 +257,14 @@ def calculateAccessKey(self, url, date): path = elements[2].split("/", 2) path = path[2].split("?") - h = hmac.new(self.accessKeySecret, self.HTTP_METHOD + "\n" - + date + "\n" - + "/" + path[0], sha1) - Signature = base64.b64encode(h.digest()) + msg = self.HTTP_METHOD + "\n" + date + "\n" + "/" + path[0] + if isinstance(msg, str): + msg = msg.encode('utf-8') + secret = self.accessKeySecret + if isinstance(secret, str): + secret = secret.encode('utf-8') + h = hmac.new(secret, msg, sha1) + Signature = base64.b64encode(h.digest()).decode('utf-8') return "ZStack %s:%s" % (self.accessKeyId, Signature) def call(self, cb=None): @@ -354,7 +362,21 @@ def _poll_result(self, rsp, cb): m = json.loads(rsp.data) location = m[LOCATION] if not location: - raise SdkError("Internal Error] the api[%s] is an async API but the server doesn't return the polling location url") + raise SdkError("[Internal Error] the api[%s] is an async API but the server doesn't return the polling location url" % self.PATH) + + # Rewrite poll URL to use client-configured hostname:port, + # in case server returns an internal IP unreachable from client + try: + from urllib.parse import urlparse, urlunparse + except ImportError: + from urlparse import urlparse, urlunparse + parsed = urlparse(location) + configured_host = __config__[CONFIG_HOSTNAME] + configured_port = str(__config__[CONFIG_PORT]) + if ':' in configured_host and not configured_host.startswith('['): + configured_host = '[%s]' % configured_host + location = urlunparse(parsed._replace( + netloc='%s:%s' % (configured_host, configured_port))) if cb: # async polling @@ -472,23 +494,34 @@ def _uuid(): def _json_http( uri, body=None, - headers={}, + headers=None, method='POST', timeout=120.0 ): pool = urllib3.PoolManager(timeout=timeout, retries=urllib3.util.retry.Retry(15)) + if headers is None: + headers = {} headers.update({'Content-Type': 'application/json', 'Connection': 'close'}) if body is not None and not isinstance(body, str): body = json.dumps(body).encode('utf-8') - print '[Request]: %s url=%s, headers=%s, body=%s' % (method, uri, headers, body) + print('[Request]: %s url=%s, headers=%s, body=%s' % (method, uri, headers, body)) if body: headers['Content-Length'] = len(body) rsp = pool.request(method, uri, body=body, headers=headers) else: rsp = pool.request(method, uri, headers=headers) - print '[Response to %s %s]: status: %s, body: %s' % (method, uri, rsp.status, rsp.data) - return rsp + data = rsp.data + if isinstance(data, bytes): + data = data.decode('utf-8') + print('[Response to %s %s]: status: %s, body: %s' % (method, uri, rsp.status, data)) + + class _Rsp(object): + pass + r = _Rsp() + r.status = rsp.status + r.data = data + return r diff --git a/utils/src/main/java/org/zstack/utils/clouderrorcode/CloudOperationsErrorCode.java b/utils/src/main/java/org/zstack/utils/clouderrorcode/CloudOperationsErrorCode.java index 6a4087cae00..4a0eaf20428 100644 --- a/utils/src/main/java/org/zstack/utils/clouderrorcode/CloudOperationsErrorCode.java +++ b/utils/src/main/java/org/zstack/utils/clouderrorcode/CloudOperationsErrorCode.java @@ -364,6 +364,8 @@ public class CloudOperationsErrorCode { public static final String ORG_ZSTACK_ZWATCH_10004 = "ORG_ZSTACK_ZWATCH_10004"; + public static final String ORG_ZSTACK_ZWATCH_10005 = "ORG_ZSTACK_ZWATCH_10005"; + public static final String ORG_ZSTACK_SSO_SAML2_SERVICE_10000 = "ORG_ZSTACK_SSO_SAML2_SERVICE_10000"; public static final String ORG_ZSTACK_SSO_SAML2_SERVICE_10001 = "ORG_ZSTACK_SSO_SAML2_SERVICE_10001"; @@ -970,6 +972,12 @@ public class CloudOperationsErrorCode { public static final String ORG_ZSTACK_NETWORK_L3_10078 = "ORG_ZSTACK_NETWORK_L3_10078"; + public static final String ORG_ZSTACK_NETWORK_L3_10079 = "ORG_ZSTACK_NETWORK_L3_10079"; + + public static final String ORG_ZSTACK_NETWORK_L3_10080 = "ORG_ZSTACK_NETWORK_L3_10080"; + + public static final String ORG_ZSTACK_NETWORK_L3_10081 = "ORG_ZSTACK_NETWORK_L3_10081"; + public static final String ORG_ZSTACK_SNS_PLATFORM_UNIVERSALSMS_SUPPLIER_EMAY_10000 = "ORG_ZSTACK_SNS_PLATFORM_UNIVERSALSMS_SUPPLIER_EMAY_10000"; public static final String ORG_ZSTACK_CORE_VALIDATION_10000 = "ORG_ZSTACK_CORE_VALIDATION_10000"; @@ -1016,6 +1024,8 @@ public class CloudOperationsErrorCode { public static final String ORG_ZSTACK_NETWORK_L2_10020 = "ORG_ZSTACK_NETWORK_L2_10020"; + public static final String ORG_ZSTACK_NETWORK_L2_10021 = "ORG_ZSTACK_NETWORK_L2_10021"; + public static final String ORG_ZSTACK_CONSOLE_10000 = "ORG_ZSTACK_CONSOLE_10000"; public static final String ORG_ZSTACK_CONSOLE_10001 = "ORG_ZSTACK_CONSOLE_10001"; @@ -3234,6 +3244,8 @@ public class CloudOperationsErrorCode { public static final String ORG_ZSTACK_NETWORK_SERVICE_PORTFORWARDING_10024 = "ORG_ZSTACK_NETWORK_SERVICE_PORTFORWARDING_10024"; + public static final String ORG_ZSTACK_NETWORK_SERVICE_PORTFORWARDING_10025 = "ORG_ZSTACK_NETWORK_SERVICE_PORTFORWARDING_10025"; + public static final String ORG_ZSTACK_STORAGE_DEVICE_10000 = "ORG_ZSTACK_STORAGE_DEVICE_10000"; public static final String ORG_ZSTACK_STORAGE_DEVICE_10001 = "ORG_ZSTACK_STORAGE_DEVICE_10001"; @@ -5482,6 +5494,16 @@ public class CloudOperationsErrorCode { public static final String ORG_ZSTACK_BAREMETAL2_GATEWAY_10083 = "ORG_ZSTACK_BAREMETAL2_GATEWAY_10083"; + public static final String ORG_ZSTACK_BAREMETAL2_GATEWAY_10084 = "ORG_ZSTACK_BAREMETAL2_GATEWAY_10084"; + + public static final String ORG_ZSTACK_BAREMETAL2_GATEWAY_10085 = "ORG_ZSTACK_BAREMETAL2_GATEWAY_10085"; + + public static final String ORG_ZSTACK_BAREMETAL2_DPU_10000 = "ORG_ZSTACK_BAREMETAL2_DPU_10000"; + + public static final String ORG_ZSTACK_BAREMETAL2_DPU_10001 = "ORG_ZSTACK_BAREMETAL2_DPU_10001"; + + public static final String ORG_ZSTACK_BAREMETAL2_DPU_10002 = "ORG_ZSTACK_BAREMETAL2_DPU_10002"; + public static final String ORG_ZSTACK_STORAGE_PRIMARY_SHAREDBLOCK_10000 = "ORG_ZSTACK_STORAGE_PRIMARY_SHAREDBLOCK_10000"; public static final String ORG_ZSTACK_STORAGE_PRIMARY_SHAREDBLOCK_10001 = "ORG_ZSTACK_STORAGE_PRIMARY_SHAREDBLOCK_10001"; @@ -6858,6 +6880,12 @@ public class CloudOperationsErrorCode { public static final String ORG_ZSTACK_PCIDEVICE_SPECIFICATION_10034 = "ORG_ZSTACK_PCIDEVICE_SPECIFICATION_10034"; + public static final String ORG_ZSTACK_PCIDEVICE_SPECIFICATION_10035 = "ORG_ZSTACK_PCIDEVICE_SPECIFICATION_10035"; + + public static final String ORG_ZSTACK_PCIDEVICE_SPECIFICATION_10036 = "ORG_ZSTACK_PCIDEVICE_SPECIFICATION_10036"; + + public static final String ORG_ZSTACK_PCIDEVICE_SPECIFICATION_10037 = "ORG_ZSTACK_PCIDEVICE_SPECIFICATION_10037"; + public static final String ORG_ZSTACK_XDRAGON_10000 = "ORG_ZSTACK_XDRAGON_10000"; public static final String ORG_ZSTACK_PLUGINPREMIUM_COMPUTE_ALLOCATOR_10000 = "ORG_ZSTACK_PLUGINPREMIUM_COMPUTE_ALLOCATOR_10000"; @@ -7700,6 +7728,12 @@ public class CloudOperationsErrorCode { public static final String ORG_ZSTACK_BAREMETAL2_INSTANCE_10089 = "ORG_ZSTACK_BAREMETAL2_INSTANCE_10089"; + public static final String ORG_ZSTACK_BAREMETAL2_INSTANCE_10090 = "ORG_ZSTACK_BAREMETAL2_INSTANCE_10090"; + + public static final String ORG_ZSTACK_BAREMETAL2_INSTANCE_10091 = "ORG_ZSTACK_BAREMETAL2_INSTANCE_10091"; + + public static final String ORG_ZSTACK_BAREMETAL2_INSTANCE_10092 = "ORG_ZSTACK_BAREMETAL2_INSTANCE_10092"; + public static final String ORG_ZSTACK_CRYPTO_SECURITYMACHINE_SECRETRESOURCEPOOL_10000 = "ORG_ZSTACK_CRYPTO_SECURITYMACHINE_SECRETRESOURCEPOOL_10000"; public static final String ORG_ZSTACK_CRYPTO_SECURITYMACHINE_SECRETRESOURCEPOOL_10001 = "ORG_ZSTACK_CRYPTO_SECURITYMACHINE_SECRETRESOURCEPOOL_10001"; @@ -9836,6 +9870,26 @@ public class CloudOperationsErrorCode { public static final String ORG_ZSTACK_COMPUTE_VM_10320 = "ORG_ZSTACK_COMPUTE_VM_10320"; + public static final String ORG_ZSTACK_COMPUTE_VM_10321 = "ORG_ZSTACK_COMPUTE_VM_10321"; + + public static final String ORG_ZSTACK_COMPUTE_VM_10322 = "ORG_ZSTACK_COMPUTE_VM_10322"; + + public static final String ORG_ZSTACK_COMPUTE_VM_10323 = "ORG_ZSTACK_COMPUTE_VM_10323"; + + public static final String ORG_ZSTACK_COMPUTE_VM_10324 = "ORG_ZSTACK_COMPUTE_VM_10324"; + + public static final String ORG_ZSTACK_COMPUTE_VM_10325 = "ORG_ZSTACK_COMPUTE_VM_10325"; + + public static final String ORG_ZSTACK_COMPUTE_VM_10326 = "ORG_ZSTACK_COMPUTE_VM_10326"; + + public static final String ORG_ZSTACK_COMPUTE_VM_10327 = "ORG_ZSTACK_COMPUTE_VM_10327"; + + public static final String ORG_ZSTACK_COMPUTE_VM_10328 = "ORG_ZSTACK_COMPUTE_VM_10328"; + + public static final String ORG_ZSTACK_COMPUTE_VM_10329 = "ORG_ZSTACK_COMPUTE_VM_10329"; + + public static final String ORG_ZSTACK_COMPUTE_VM_10330 = "ORG_ZSTACK_COMPUTE_VM_10330"; + public static final String ORG_ZSTACK_IDENTITY_LOGIN_10000 = "ORG_ZSTACK_IDENTITY_LOGIN_10000"; public static final String ORG_ZSTACK_STORAGE_VOLUME_BLOCK_EXPON_10000 = "ORG_ZSTACK_STORAGE_VOLUME_BLOCK_EXPON_10000"; @@ -10388,6 +10442,7 @@ public class CloudOperationsErrorCode { public static final String ORG_ZSTACK_NETWORK_SECURITYGROUP_10129 = "ORG_ZSTACK_NETWORK_SECURITYGROUP_10129"; + public static final String ORG_ZSTACK_TEMPLATECONFIG_10000 = "ORG_ZSTACK_TEMPLATECONFIG_10000"; public static final String ORG_ZSTACK_TEMPLATECONFIG_10001 = "ORG_ZSTACK_TEMPLATECONFIG_10001"; @@ -13200,6 +13255,10 @@ public class CloudOperationsErrorCode { public static final String ORG_ZSTACK_CORE_THREAD_10002 = "ORG_ZSTACK_CORE_THREAD_10002"; + public static final String ORG_ZSTACK_CORE_THREAD_10003 = "ORG_ZSTACK_CORE_THREAD_10003"; + + public static final String ORG_ZSTACK_CORE_THREAD_10004 = "ORG_ZSTACK_CORE_THREAD_10004"; + public static final String ORG_ZSTACK_POLICYROUTE_10000 = "ORG_ZSTACK_POLICYROUTE_10000"; public static final String ORG_ZSTACK_POLICYROUTE_10001 = "ORG_ZSTACK_POLICYROUTE_10001"; @@ -13612,6 +13671,16 @@ public class CloudOperationsErrorCode { public static final String ORG_ZSTACK_NETWORK_SERVICE_LB_10171 = "ORG_ZSTACK_NETWORK_SERVICE_LB_10171"; + public static final String ORG_ZSTACK_NETWORK_SERVICE_LB_10172 = "ORG_ZSTACK_NETWORK_SERVICE_LB_10172"; + + public static final String ORG_ZSTACK_NETWORK_SERVICE_LB_10173 = "ORG_ZSTACK_NETWORK_SERVICE_LB_10173"; + + public static final String ORG_ZSTACK_NETWORK_SERVICE_LB_10174 = "ORG_ZSTACK_NETWORK_SERVICE_LB_10174"; + + public static final String ORG_ZSTACK_NETWORK_SERVICE_LB_10175 = "ORG_ZSTACK_NETWORK_SERVICE_LB_10175"; + + public static final String ORG_ZSTACK_NETWORK_SERVICE_LB_10176 = "ORG_ZSTACK_NETWORK_SERVICE_LB_10176"; + public static final String ORG_ZSTACK_IPSEC_10000 = "ORG_ZSTACK_IPSEC_10000"; public static final String ORG_ZSTACK_IPSEC_10001 = "ORG_ZSTACK_IPSEC_10001"; @@ -13742,6 +13811,8 @@ public class CloudOperationsErrorCode { public static final String ORG_ZSTACK_BAREMETAL2_CHASSIS_10025 = "ORG_ZSTACK_BAREMETAL2_CHASSIS_10025"; + public static final String ORG_ZSTACK_BAREMETAL2_CHASSIS_10026 = "ORG_ZSTACK_BAREMETAL2_CHASSIS_10026"; + public static final String ORG_ZSTACK_BAREMETAL2_CLUSTER_10000 = "ORG_ZSTACK_BAREMETAL2_CLUSTER_10000"; public static final String ORG_ZSTACK_BAREMETAL2_CLUSTER_10001 = "ORG_ZSTACK_BAREMETAL2_CLUSTER_10001"; @@ -14816,6 +14887,41 @@ public class CloudOperationsErrorCode { public static final String ORG_ZSTACK_AI_10134 = "ORG_ZSTACK_AI_10134"; + public static final String ORG_ZSTACK_AI_10135 = "ORG_ZSTACK_AI_10135"; + + public static final String ORG_ZSTACK_AI_10136 = "ORG_ZSTACK_AI_10136"; + + public static final String ORG_ZSTACK_AI_10137 = "ORG_ZSTACK_AI_10137"; + + public static final String ORG_ZSTACK_AI_10138 = "ORG_ZSTACK_AI_10138"; + + public static final String ORG_ZSTACK_AI_10139 = "ORG_ZSTACK_AI_10139"; + + public static final String ORG_ZSTACK_AI_10140 = "ORG_ZSTACK_AI_10140"; + + public static final String ORG_ZSTACK_AI_10141 = "ORG_ZSTACK_AI_10141"; + + public static final String ORG_ZSTACK_AI_10142 = "ORG_ZSTACK_AI_10142"; + + public static final String ORG_ZSTACK_AI_10143 = "ORG_ZSTACK_AI_10143"; + + public static final String ORG_ZSTACK_AI_10144 = "ORG_ZSTACK_AI_10144"; + + public static final String ORG_ZSTACK_AI_10145 = "ORG_ZSTACK_AI_10145"; + + public static final String ORG_ZSTACK_AI_10146 = "ORG_ZSTACK_AI_10146"; + + public static final String ORG_ZSTACK_AI_10147 = "ORG_ZSTACK_AI_10147"; + + public static final String ORG_ZSTACK_AI_10148 = "ORG_ZSTACK_AI_10148"; + + public static final String ORG_ZSTACK_AI_10149 = "ORG_ZSTACK_AI_10149"; + + public static final String ORG_ZSTACK_AI_10150 = "ORG_ZSTACK_AI_10150"; + + public static final String ORG_ZSTACK_AI_10157 = "ORG_ZSTACK_AI_10157"; + public static final String ORG_ZSTACK_AI_10158 = "ORG_ZSTACK_AI_10158"; + public static final String ORG_ZSTACK_CORE_CLOUDBUS_10000 = "ORG_ZSTACK_CORE_CLOUDBUS_10000"; public static final String ORG_ZSTACK_CORE_CLOUDBUS_10001 = "ORG_ZSTACK_CORE_CLOUDBUS_10001"; @@ -15132,6 +15238,8 @@ public class CloudOperationsErrorCode { public static final String ORG_ZSTACK_NETWORK_SERVICE_EIP_10023 = "ORG_ZSTACK_NETWORK_SERVICE_EIP_10023"; + public static final String ORG_ZSTACK_NETWORK_SERVICE_EIP_10024 = "ORG_ZSTACK_NETWORK_SERVICE_EIP_10024"; + public static final String ORG_ZSTACK_TEST_INTEGRATION_GUESTTOOLS_10000 = "ORG_ZSTACK_TEST_INTEGRATION_GUESTTOOLS_10000"; public static final String ORG_ZSTACK_TEST_INTEGRATION_GUESTTOOLS_10001 = "ORG_ZSTACK_TEST_INTEGRATION_GUESTTOOLS_10001"; @@ -15885,4 +15993,28 @@ public class CloudOperationsErrorCode { public static final String ORG_ZSTACK_CRYPTO_SECURITYMACHINE_THIRDPARTY_PLUGIN_10005 = "ORG_ZSTACK_CRYPTO_SECURITYMACHINE_THIRDPARTY_PLUGIN_10005"; public static final String ORG_ZSTACK_CRYPTO_SECURITYMACHINE_THIRDPARTY_PLUGIN_10006 = "ORG_ZSTACK_CRYPTO_SECURITYMACHINE_THIRDPARTY_PLUGIN_10006"; + + public static final String ORG_ZSTACK_DGPU_10001 = "ORG_ZSTACK_DGPU_10001"; + + public static final String ORG_ZSTACK_DGPU_10002 = "ORG_ZSTACK_DGPU_10002"; + + public static final String ORG_ZSTACK_DGPU_10003 = "ORG_ZSTACK_DGPU_10003"; + + public static final String ORG_ZSTACK_DGPU_10004 = "ORG_ZSTACK_DGPU_10004"; + + public static final String ORG_ZSTACK_DGPU_10005 = "ORG_ZSTACK_DGPU_10005"; + + public static final String ORG_ZSTACK_DGPU_10006 = "ORG_ZSTACK_DGPU_10006"; + + public static final String ORG_ZSTACK_DGPU_10007 = "ORG_ZSTACK_DGPU_10007"; + + public static final String ORG_ZSTACK_DGPU_10008 = "ORG_ZSTACK_DGPU_10008"; + + public static final String ORG_ZSTACK_DGPU_10009 = "ORG_ZSTACK_DGPU_10009"; + + public static final String ORG_ZSTACK_DGPU_10010 = "ORG_ZSTACK_DGPU_10010"; + + public static final String ORG_ZSTACK_DGPU_10011 = "ORG_ZSTACK_DGPU_10011"; + + public static final String ORG_ZSTACK_DGPU_10012 = "ORG_ZSTACK_DGPU_10012"; } diff --git a/utils/src/main/java/org/zstack/utils/network/NicIpAddressInfo.java b/utils/src/main/java/org/zstack/utils/network/NicIpAddressInfo.java index 9803946ee25..f190a2f278b 100644 --- a/utils/src/main/java/org/zstack/utils/network/NicIpAddressInfo.java +++ b/utils/src/main/java/org/zstack/utils/network/NicIpAddressInfo.java @@ -1,5 +1,7 @@ package org.zstack.utils.network; +import java.util.List; + public class NicIpAddressInfo { public String ipv4Address; public String ipv4Gateway; @@ -7,6 +9,7 @@ public class NicIpAddressInfo { public String ipv6Address; public String ipv6Gateway; public String ipv6Prefix; + public List dnsAddresses; public NicIpAddressInfo(String ipv4Address, String ipv4Gateway, String ipv4Netmask, String ipv6Address, String ipv6Gateway, String ipv6Prefix) { this.ipv4Address = ipv4Address; @@ -15,5 +18,6 @@ public NicIpAddressInfo(String ipv4Address, String ipv4Gateway, String ipv4Netma this.ipv6Address = ipv6Address; this.ipv6Gateway = ipv6Gateway; this.ipv6Prefix = ipv6Prefix; + this.dnsAddresses = null; } }