diff --git a/.gitignore b/.gitignore index 641f731fe03..e823298f7f6 100755 --- a/.gitignore +++ b/.gitignore @@ -34,3 +34,5 @@ envDSLTree test/zstack-integration-test-result/ premium/test-premium/zstack-api.log **/bin/ +CLAUDE.md +.claude/* diff --git a/VERSION b/VERSION index f36c3dfd747..6d937c2fb57 100755 --- a/VERSION +++ b/VERSION @@ -1,3 +1,3 @@ MAJOR=5 MINOR=5 -UPDATE=6 +UPDATE=12 diff --git a/build/pom.xml b/build/pom.xml index 6801d25cbba..a1ea6a47ca7 100755 --- a/build/pom.xml +++ b/build/pom.xml @@ -731,9 +731,9 @@ io.sentry sentry-opentelemetry-bootstrap - + - io.opentelemetry + io.opentelemetry.semconv opentelemetry-semconv diff --git a/compute/src/main/java/org/zstack/compute/allocator/HostAllocatorChain.java b/compute/src/main/java/org/zstack/compute/allocator/HostAllocatorChain.java index 2262251d02e..371d1dfd355 100755 --- a/compute/src/main/java/org/zstack/compute/allocator/HostAllocatorChain.java +++ b/compute/src/main/java/org/zstack/compute/allocator/HostAllocatorChain.java @@ -146,7 +146,14 @@ private void runFlow(AbstractHostAllocatorFlow flow) { } } catch (Throwable t) { logger.warn("unhandled throwable", t); - completion.fail(inerr(ORG_ZSTACK_COMPUTE_ALLOCATOR_10019, t.toString())); + String errMsg = t != null ? t.toString() : "unknown error"; + if (isDryRun) { + if (dryRunCompletion != null) { + dryRunCompletion.fail(inerr(ORG_ZSTACK_COMPUTE_ALLOCATOR_10019, errMsg)); + } + } else if (completion != null) { + completion.fail(inerr(ORG_ZSTACK_COMPUTE_ALLOCATOR_10019, errMsg)); + } } } diff --git a/compute/src/main/java/org/zstack/compute/allocator/HostAllocatorManagerImpl.java b/compute/src/main/java/org/zstack/compute/allocator/HostAllocatorManagerImpl.java index 48544a68333..a6598082004 100755 --- a/compute/src/main/java/org/zstack/compute/allocator/HostAllocatorManagerImpl.java +++ b/compute/src/main/java/org/zstack/compute/allocator/HostAllocatorManagerImpl.java @@ -851,17 +851,15 @@ public HostCapacityVO call(HostCapacityVO cap) { long deltaMemory = ratioMgr.calculateMemoryByRatio(hostUuid, memory); long availMemory = cap.getAvailableMemory() + deltaMemory; if (availMemory > cap.getTotalMemory()) { - throw new CloudRuntimeException( - String.format("invalid memory capacity of host[uuid:%s]," + - " available memory[%s] is greater than total memory[%s]." + - " Available Memory before is [%s], Delta Memory is [%s].", - hostUuid, - new DecimalFormat(",###").format(availMemory), - new DecimalFormat(",###").format(cap.getTotalMemory()), - new DecimalFormat(",###").format(cap.getAvailableMemory()), - new DecimalFormat(",###").format(deltaMemory) - ) - ); + logger.warn(String.format("memory capacity overflow on host[uuid:%s]," + + " available memory[%s] > total memory[%s], clamping to total." + + " Available Memory before is [%s], Delta Memory is [%s].", + hostUuid, + new DecimalFormat(",###").format(availMemory), + new DecimalFormat(",###").format(cap.getTotalMemory()), + new DecimalFormat(",###").format(cap.getAvailableMemory()), + new DecimalFormat(",###").format(deltaMemory))); + availMemory = cap.getTotalMemory(); } cap.setAvailableMemory(availMemory); diff --git a/compute/src/main/java/org/zstack/compute/host/HostBase.java b/compute/src/main/java/org/zstack/compute/host/HostBase.java index 38eb654804f..919d92631fc 100755 --- a/compute/src/main/java/org/zstack/compute/host/HostBase.java +++ b/compute/src/main/java/org/zstack/compute/host/HostBase.java @@ -411,9 +411,8 @@ public void run(final FlowTrigger trigger, Map data) { if (ordered != null) { vmUuids = ordered; - logger.debug(String.format("%s ordered VMs for host maintenance, to keep the order, we will migrate VMs one by one", - ext.getClass())); - migrateQuantity = 1; + logger.debug(String.format("%s ordered VMs for host maintenance, migrate quantity: %d", + ext.getClass(), migrateQuantity)); } } @@ -1443,7 +1442,7 @@ public String getName() { @Override protected String getDeduplicateString() { - return String.format("connect-host-%s", self.getUuid()); + return String.format("connect-host-%s", self == null ? "unknown" : self.getUuid()); } }); } 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/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/VmCapabilitiesJudger.java b/compute/src/main/java/org/zstack/compute/vm/VmCapabilitiesJudger.java index 92054658fb2..d3ceeb603b8 100644 --- a/compute/src/main/java/org/zstack/compute/vm/VmCapabilitiesJudger.java +++ b/compute/src/main/java/org/zstack/compute/vm/VmCapabilitiesJudger.java @@ -53,6 +53,12 @@ private void checkPrimaryStorageCapabilities(VmCapabilities capabilities, VmInst q.add(PrimaryStorageVO_.uuid, SimpleQuery.Op.EQ, rootVolume.getPrimaryStorageUuid()); String type = q.findValue(); + if (type == null) { + capabilities.setSupportLiveMigration(false); + capabilities.setSupportVolumeMigration(false); + return; + } + PrimaryStorageType psType = PrimaryStorageType.valueOf(type); if (vm.getState() != VmInstanceState.Running) { 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 80ba38486fc..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 @@ -1603,6 +1652,11 @@ public void handle(Map data) { changeVmStateInDb(VmInstanceStateEvent.paused, () -> self.setHostUuid(currentHostUuid)); } else if (currentState == VmInstanceState.Stopped) { changeVmStateInDb(VmInstanceStateEvent.stopped); + } else if (currentState == VmInstanceState.NoState) { + // ZSTAC-80898: When host reports NoState (e.g. QEMU crashed, libvirtd restarted), + // update DB to reflect actual state. Without this, VMs in intermediate states + // (Migrating, Starting, etc.) remain stuck in DB forever. + changeVmStateInDb(VmInstanceStateEvent.noState, () -> self.setHostUuid(currentHostUuid)); } fireEvent.run(); @@ -3475,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 @@ -3501,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() { @@ -3646,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(); } }); @@ -3674,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) { @@ -3684,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(); } @@ -5431,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()); } } @@ -6189,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); } @@ -6219,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 @@ -6234,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 @@ -6246,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() { @@ -6266,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() { @@ -6310,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); @@ -6364,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; } @@ -6439,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; } @@ -6611,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(); @@ -6635,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/VmNicManagerImpl.java b/compute/src/main/java/org/zstack/compute/vm/VmNicManagerImpl.java index 25c0b005d71..31b3e35d32a 100644 --- a/compute/src/main/java/org/zstack/compute/vm/VmNicManagerImpl.java +++ b/compute/src/main/java/org/zstack/compute/vm/VmNicManagerImpl.java @@ -58,6 +58,10 @@ public void afterAddIpAddress(String vmNicUUid, String usedIpUuid) { SQL.New(UsedIpVO.class).eq(UsedIpVO_.uuid, usedIpUuid).set(UsedIpVO_.vmNicUuid, vmNicUUid).update(); VmNicVO nic = Q.New(VmNicVO.class).eq(VmNicVO_.uuid, vmNicUUid).find(); + if (nic == null) { + logger.debug(String.format("VmNic[uuid:%s] not found, skip afterAddIpAddress", vmNicUUid)); + return; + } UsedIpVO temp = null; /* if there is ipv4 addresses, we put the first attached ipv4 address to VmNic.ip @@ -88,6 +92,10 @@ public void afterAddIpAddress(String vmNicUUid, String usedIpUuid) { @Override public void afterDelIpAddress(String vmNicUUid, String usedIpUuid) { VmNicVO nic = Q.New(VmNicVO.class).eq(VmNicVO_.uuid, vmNicUUid).find(); + if (nic == null) { + logger.debug(String.format("VmNic[uuid:%s] not found, skip afterDelIpAddress", vmNicUUid)); + return; + } if (nic.getUsedIpUuid() != null && !nic.getUsedIpUuid().equals(usedIpUuid)) { return; } 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 10dfb0d7610..bb63bbc9991 100644 --- a/compute/src/main/java/org/zstack/compute/vm/VmQuotaOperator.java +++ b/compute/src/main/java/org/zstack/compute/vm/VmQuotaOperator.java @@ -15,6 +15,7 @@ import org.zstack.header.image.ImageConstant; import org.zstack.header.message.APIMessage; import org.zstack.header.message.NeedQuotaCheckMessage; +import org.zstack.header.configuration.InstanceOfferingVO; import org.zstack.header.vm.*; import org.zstack.header.volume.*; import org.zstack.identity.QuotaUtil; @@ -59,6 +60,10 @@ public void checkQuota(APIMessage msg, Map pairs) { check((APIChangeResourceOwnerMsg) msg, pairs); } else if (msg instanceof APIRecoverVmInstanceMsg) { check((APIRecoverVmInstanceMsg) msg, pairs); + } else if (msg instanceof APIChangeInstanceOfferingMsg) { + check((APIChangeInstanceOfferingMsg) msg, pairs); + } else if (msg instanceof APIUpdateVmInstanceMsg) { + check((APIUpdateVmInstanceMsg) msg, pairs); } } else { if (msg instanceof APIChangeResourceOwnerMsg) { @@ -227,6 +232,66 @@ public void checkVmCupAndMemoryCapacity(String currentAccountUuid, String resour } } + @Transactional(readOnly = true) + private void check(APIChangeInstanceOfferingMsg msg, Map pairs) { + VmInstanceVO vm = dbf.findByUuid(msg.getVmInstanceUuid(), VmInstanceVO.class); + if (!VmInstanceState.Running.equals(vm.getState())) { + return; + } + InstanceOfferingVO newOffering = dbf.findByUuid(msg.getInstanceOfferingUuid(), InstanceOfferingVO.class); + long cpuDelta = newOffering.getCpuNum() - vm.getCpuNum(); + long memoryDelta = newOffering.getMemorySize() - vm.getMemorySize(); + String currentAccountUuid = msg.getSession().getAccountUuid(); + String resourceOwnerAccountUuid = new QuotaUtil().getResourceOwnerAccountUuid(msg.getVmInstanceUuid()); + checkVmCpuAndMemoryDelta(currentAccountUuid, resourceOwnerAccountUuid, cpuDelta, memoryDelta, pairs); + } + + @Transactional(readOnly = true) + private void check(APIUpdateVmInstanceMsg msg, Map pairs) { + if (msg.getCpuNum() == null && msg.getMemorySize() == null) { + return; + } + VmInstanceVO vm = dbf.findByUuid(msg.getVmInstanceUuid(), VmInstanceVO.class); + if (!VmInstanceState.Running.equals(vm.getState())) { + return; + } + long cpuDelta = msg.getCpuNum() != null ? msg.getCpuNum() - vm.getCpuNum() : 0; + long memoryDelta = msg.getMemorySize() != null ? msg.getMemorySize() - vm.getMemorySize() : 0; + String currentAccountUuid = msg.getSession().getAccountUuid(); + String resourceOwnerAccountUuid = new QuotaUtil().getResourceOwnerAccountUuid(msg.getVmInstanceUuid()); + checkVmCpuAndMemoryDelta(currentAccountUuid, resourceOwnerAccountUuid, cpuDelta, memoryDelta, pairs); + } + + @Transactional(readOnly = true) + private void checkVmCpuAndMemoryDelta(String currentAccountUuid, String resourceOwnerAccountUuid, + long cpuDelta, long memoryDelta, + Map pairs) { + if (cpuDelta <= 0 && memoryDelta <= 0) { + return; + } + VmQuotaUtil.VmQuota vmQuotaUsed = new VmQuotaUtil().getUsedVmCpuMemory(resourceOwnerAccountUuid); + if (cpuDelta > 0) { + QuotaUtil.QuotaCompareInfo quotaCompareInfo = new QuotaUtil.QuotaCompareInfo(); + quotaCompareInfo.currentAccountUuid = currentAccountUuid; + quotaCompareInfo.resourceTargetOwnerAccountUuid = resourceOwnerAccountUuid; + quotaCompareInfo.quotaName = VmQuotaConstant.VM_RUNNING_CPU_NUM; + quotaCompareInfo.quotaValue = pairs.get(VmQuotaConstant.VM_RUNNING_CPU_NUM).getValue(); + quotaCompareInfo.currentUsed = vmQuotaUsed.runningVmCpuNum; + quotaCompareInfo.request = cpuDelta; + new QuotaUtil().CheckQuota(quotaCompareInfo); + } + if (memoryDelta > 0) { + QuotaUtil.QuotaCompareInfo quotaCompareInfo = new QuotaUtil.QuotaCompareInfo(); + quotaCompareInfo.currentAccountUuid = currentAccountUuid; + quotaCompareInfo.resourceTargetOwnerAccountUuid = resourceOwnerAccountUuid; + quotaCompareInfo.quotaName = VmQuotaConstant.VM_RUNNING_MEMORY_SIZE; + quotaCompareInfo.quotaValue = pairs.get(VmQuotaConstant.VM_RUNNING_MEMORY_SIZE).getValue(); + quotaCompareInfo.currentUsed = vmQuotaUsed.runningVmMemorySize; + quotaCompareInfo.request = memoryDelta; + new QuotaUtil().CheckQuota(quotaCompareInfo); + } + } + private void checkVolumeQuotaForChangeResourceOwner(List dataVolumeUuids, List rootVolumeUuids, String resourceTargetOwnerAccountUuid, 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/conf/db/upgrade/V5.5.12__schema.sql b/conf/db/upgrade/V5.5.12__schema.sql new file mode 100644 index 00000000000..6c6168445f8 --- /dev/null +++ b/conf/db/upgrade/V5.5.12__schema.sql @@ -0,0 +1,180 @@ +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; \ No newline at end of file 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 41e1915fedf..1e24c52ea11 100644 --- a/conf/i18n/globalErrorCodeMapping/global-error-de-DE.json +++ b/conf/i18n/globalErrorCodeMapping/global-error-de-DE.json @@ -36,9 +36,10 @@ "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 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]", - "ORG_ZSTACK_STORAGE_PRIMARY_10052": "primaryStorageUuid-Konflikt: Der durch das Plattenangebot angegebene primäre Speicher ist %s, während der durch den Erstellungsparameter angegebene primäre Speicher %s ist.", + "ORG_ZSTACK_STORAGE_PRIMARY_10052": "primaryStorageUuid-Konflikt: Der durch das Datenplattenangebot angegebene primäre Speicher ist %s, während der durch den Erstellungsparameter angegebene primäre Speicher %s ist.", "ORG_ZSTACK_V2V_10006": "Die zugrunde liegende Compute-Instanz des Konversions-Hosts[uuid:%s] sollte verbunden sein", "ORG_ZSTACK_V2V_10007": "Konversions-Host[uuid:%s] kann keine Verbindung zum primären Speicher[uuid:%s] herstellen", "ORG_ZSTACK_V2V_10004": "VM-Instanz [UUID:%s] existiert nicht oder ist keine VMware-VM-Instanz", @@ -629,6 +630,7 @@ "ORG_ZSTACK_CONFIGURATION_10006": "Instanztyp[%s] wird nicht unterstützt", "ORG_ZSTACK_MULTICAST_ROUTER_10007": "Gruppenadresse [%s] ist keine Multicast-Gruppenadresse", "ORG_ZSTACK_STORAGE_ADDON_PRIMARY_10014": "Speicher ist nicht gesund:%s", + "ORG_ZSTACK_STORAGE_ADDON_PRIMARY_10015": "Kein verfügbarer Backup-Speicher mit bevorzugten Typen %s für primären Speicher[uuid:%s]", "ORG_ZSTACK_MULTICAST_ROUTER_10000": "multicastRouter[uuid:%s] wurde keinem VPC-Router zugeordnet", "ORG_ZSTACK_MULTICAST_ROUTER_10001": "Multicast ist bereits auf dem VPC-Router UUID[:%s] aktiviert", "ORG_ZSTACK_STORAGE_ADDON_PRIMARY_10012": "Der Eigentümer-Volume-Pfad kann aus dem internen Snapshot-Pfad[%s] nicht gefunden werden, da der reguläre Ausdruck[%s] nicht mit dem Snapshot-Pfad übereinstimmt", @@ -3424,6 +3426,7 @@ "ORG_ZSTACK_STORAGE_PRIMARY_BLOCK_10038": "Volume-Vorlage mit dem Namen %s konnte nicht gefunden werden", "ORG_ZSTACK_MEVOCO_10076": "shareable Volume(s) [UUID: %s] angehängt; Gruppensnapshot wird nicht unterstützt.", "ORG_ZSTACK_AI_MESSAGE_10002": "Dataset-Erstellungsauftrag kann nicht abgebrochen werden, weil die Ressourcen-UUID[%s] oder Model-Center-UUID[%s] null ist", + "ORG_ZSTACK_AI_MESSAGE_10003": "Die Modell-Download-Aufgabe muss neu gestartet werden", "ORG_ZSTACK_AI_MESSAGE_10001": "Model-Service-Download-Aufgabe kann nicht abgebrochen werden, weil die Ressourcen-UUID[%s] oder Model-Center-UUID[%s] null ist", "ORG_ZSTACK_IPSEC_VYOS_10009": "Anwendung auf HA-Gruppe fehlgeschlagen wegen %s", "ORG_ZSTACK_PREMIUM_EXTERNALSERVICE_PROMETHEUS_10013": "HTTP-Aufrufe an alle Prometheus-Instanzen in der Cloud-Umgebung fehlgeschlagen.", diff --git a/conf/i18n/globalErrorCodeMapping/global-error-en_US.json b/conf/i18n/globalErrorCodeMapping/global-error-en_US.json index 32eb4c8f056..7df63d4098c 100644 --- a/conf/i18n/globalErrorCodeMapping/global-error-en_US.json +++ b/conf/i18n/globalErrorCodeMapping/global-error-en_US.json @@ -36,9 +36,10 @@ "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 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]", - "ORG_ZSTACK_STORAGE_PRIMARY_10052": "primaryStorageUuid Conflict: The primary storage specified by the disk offering is %s, while the primary storage specified in the creation parameter is %s.", + "ORG_ZSTACK_STORAGE_PRIMARY_10052": "primaryStorageUuid Conflict: The primary storage specified by the data disk offering is %s, while the primary storage specified in the creation parameter is %s.", "ORG_ZSTACK_V2V_10006": "underlying compute instance of conversion host[uuid:%s] should be Connected", "ORG_ZSTACK_V2V_10007": "conversion host[uuid:%s] cannot establish a connection to the primary storage[uuid:%s]", "ORG_ZSTACK_V2V_10004": "VM instance [UUID:%s] does not exist or is not a VMware VM instance", @@ -629,6 +630,7 @@ "ORG_ZSTACK_CONFIGURATION_10006": "unsupported instance type[%s]", "ORG_ZSTACK_MULTICAST_ROUTER_10007": "group address [%s] is not a multicast group address", "ORG_ZSTACK_STORAGE_ADDON_PRIMARY_10014": "storage is not healthy:%s", + "ORG_ZSTACK_STORAGE_ADDON_PRIMARY_10015": "no available backup storage with preferred types %s for primary storage[uuid:%s]", "ORG_ZSTACK_MULTICAST_ROUTER_10000": "multicastRouter[uuid:%s] has not been associated with a VPC router", "ORG_ZSTACK_MULTICAST_ROUTER_10001": "multicast already enabled on VPC router UUID[:%s]", "ORG_ZSTACK_STORAGE_ADDON_PRIMARY_10012": "cannot find the owning volume path from the internal snapshot path[%s], as the regex[%s] fails to match the snapshot path", @@ -3374,7 +3376,7 @@ "ORG_ZSTACK_NETWORK_HUAWEI_IMASTER_10019": "delete token of SDN controller [IP:%s] failed because %s", "ORG_ZSTACK_STORAGE_PRIMARY_BLOCK_10004": "Cannot execute volume mapping to host flow due to invalid volume ID.%s", "ORG_ZSTACK_NETWORK_SERVICE_PORTFORWARDING_10007": "port forwarding rule [uuid:%s] has not been attached to any virtual machine network interface, cannot detach", - "ORG_ZSTACK_MEVOCO_10088": "cannot take a snapshot for volumes[%s] when volume[uuid: %s] is not attached", + "ORG_ZSTACK_MEVOCO_10088": "cannot create snapshot for volume[uuid:%s] because it is not attached to any VM instance. Please attach the volume to a VM first. Affected volumes: %s", "ORG_ZSTACK_STORAGE_PRIMARY_BLOCK_10005": "Cannot execute map LUN to host flow due to invalid LUN type: %s", "ORG_ZSTACK_NETWORK_SERVICE_PORTFORWARDING_10008": "port forwarding rule [uuid:%s] has been associated with vm nic [uuid:%s], cannot be reassigned again", "ORG_ZSTACK_MEVOCO_10087": "A Running VM[uuid:%s] has no associated Host UUID.", @@ -3515,6 +3517,7 @@ "ORG_ZSTACK_STORAGE_PRIMARY_BLOCK_10038": "failed to find volume template by name:%s", "ORG_ZSTACK_MEVOCO_10076": "shareable volume(s) [UUID: %s] attached; group snapshot is not supported.", "ORG_ZSTACK_AI_MESSAGE_10002": "Cannot cancel dataset creation job because the resource UUID[%s] or model center UUID[%s] is null", + "ORG_ZSTACK_AI_MESSAGE_10003": "The model download task needs to restart", "ORG_ZSTACK_AI_MESSAGE_10001": "Cannot cancel model service download task because the resource UUID[%s] or model center UUID[%s] is null", "ORG_ZSTACK_IPSEC_VYOS_10009": "apply to HA group failed because %s", "ORG_ZSTACK_PREMIUM_EXTERNALSERVICE_PROMETHEUS_10013": "Failed to perform HTTP calls to all Prometheus instances in the cloud environment.", diff --git a/conf/i18n/globalErrorCodeMapping/global-error-fr-FR.json b/conf/i18n/globalErrorCodeMapping/global-error-fr-FR.json index 449344a6cef..8fb00fa5776 100644 --- a/conf/i18n/globalErrorCodeMapping/global-error-fr-FR.json +++ b/conf/i18n/globalErrorCodeMapping/global-error-fr-FR.json @@ -36,9 +36,10 @@ "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 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]", - "ORG_ZSTACK_STORAGE_PRIMARY_10052": "Conflit primaryStorageUuid : le stockage principal spécifié par l'offre de disque est %s, tandis que le stockage principal spécifié dans le paramètre de création est %s.", + "ORG_ZSTACK_STORAGE_PRIMARY_10052": "Conflit primaryStorageUuid : le stockage principal spécifié par l'offre de disque de données est %s, tandis que le stockage principal spécifié dans le paramètre de création est %s.", "ORG_ZSTACK_V2V_10006": "l'instance de calcul sous-jacente de l'hôte de conversion[uuid:%s] doit être Connectée", "ORG_ZSTACK_V2V_10007": "l'hôte de conversion[uuid:%s] ne peut pas établir de connexion avec le stockage principal[uuid:%s]", "ORG_ZSTACK_V2V_10004": "L'instance de VM [UUID:%s] n'existe pas ou n'est pas une instance de VM VMware", @@ -629,6 +630,7 @@ "ORG_ZSTACK_CONFIGURATION_10006": "type d'instance[%s] non supporté", "ORG_ZSTACK_MULTICAST_ROUTER_10007": "l'adresse de groupe [%s] n'est pas une adresse de groupe multicast", "ORG_ZSTACK_STORAGE_ADDON_PRIMARY_10014": "le stockage n'est pas sain : %s", + "ORG_ZSTACK_STORAGE_ADDON_PRIMARY_10015": "aucun stockage de sauvegarde disponible avec les types préférés %s pour le stockage principal[uuid:%s]", "ORG_ZSTACK_MULTICAST_ROUTER_10000": "Le multicastRouter[uuid:%s] n'a pas été associé à un routeur VPC", "ORG_ZSTACK_MULTICAST_ROUTER_10001": "Le multicast est déjà activé sur le routeur VPC UUID[:%s]", "ORG_ZSTACK_STORAGE_ADDON_PRIMARY_10012": "Impossible de trouver le chemin du volume propriétaire à partir du chemin d'instantané interne[%s], car l'expression régulière[%s] ne correspond pas au chemin d'instantané", @@ -3514,6 +3516,7 @@ "ORG_ZSTACK_STORAGE_PRIMARY_BLOCK_10038": "Échec de recherche du modèle de volume par nom:%s", "ORG_ZSTACK_MEVOCO_10076": "Volume(s) partageable(s) [UUID: %s] attaché(s); la capture instantanée de groupe n'est pas prise en charge.", "ORG_ZSTACK_AI_MESSAGE_10002": "Impossible d'annuler la tâche de création de l'ensemble de données car l'UUID [%s] de la ressource ou l'UUID [%s] du centre de modèles est null", + "ORG_ZSTACK_AI_MESSAGE_10003": "La tâche de téléchargement du modèle doit être redémarrée", "ORG_ZSTACK_AI_MESSAGE_10001": "Impossible d'annuler la tâche de téléchargement du service de modèle car l'UUID [%s] de la ressource ou l'UUID [%s] du centre de modèles est null", "ORG_ZSTACK_IPSEC_VYOS_10009": "Échec de l'application au groupe haute disponibilité en raison de %s", "ORG_ZSTACK_PREMIUM_EXTERNALSERVICE_PROMETHEUS_10013": "Échec de l'exécution des appels HTTP vers toutes les instances Prometheus dans l'environnement cloud.", diff --git a/conf/i18n/globalErrorCodeMapping/global-error-id-ID.json b/conf/i18n/globalErrorCodeMapping/global-error-id-ID.json index d8cd5bf487a..7e41658f314 100644 --- a/conf/i18n/globalErrorCodeMapping/global-error-id-ID.json +++ b/conf/i18n/globalErrorCodeMapping/global-error-id-ID.json @@ -36,9 +36,10 @@ "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 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]", - "ORG_ZSTACK_STORAGE_PRIMARY_10052": "Konflik primaryStorageUuid: Primary storage yang ditentukan oleh disk offering adalah %s, sementara primary storage yang ditentukan dalam parameter pembuatan adalah %s", + "ORG_ZSTACK_STORAGE_PRIMARY_10052": "Konflik primaryStorageUuid: Primary storage yang ditentukan oleh data disk offering adalah %s, sementara primary storage yang ditentukan dalam parameter pembuatan adalah %s", "ORG_ZSTACK_V2V_10006": "instance komputasi yang mendasar dari conversion host[uuid:%s] harus Connected", "ORG_ZSTACK_V2V_10007": "conversion host[uuid:%s] tidak dapat建立 koneksi ke primary storage[uuid:%s]", "ORG_ZSTACK_V2V_10004": "Instance VM [UUID:%s] tidak ada atau bukan instance VM VMware", @@ -629,6 +630,7 @@ "ORG_ZSTACK_CONFIGURATION_10006": "jenis instance[%s] tidak didukung", "ORG_ZSTACK_MULTICAST_ROUTER_10007": "alamat grup [%s] bukan alamat grup multicast", "ORG_ZSTACK_STORAGE_ADDON_PRIMARY_10014": "penyimpanan tidak sehat:%s", + "ORG_ZSTACK_STORAGE_ADDON_PRIMARY_10015": "tidak ada penyimpanan cadangan yang tersedia dengan tipe yang disukai %s untuk penyimpanan utama[uuid:%s]", "ORG_ZSTACK_MULTICAST_ROUTER_10000": "multicastRouter[uuid:%s] belum dikaitkan dengan router VPC", "ORG_ZSTACK_MULTICAST_ROUTER_10001": "multicast sudah diaktifkan pada router VPC UUID[:%s]", "ORG_ZSTACK_STORAGE_ADDON_PRIMARY_10012": "tidak dapat menemukan jalur volume pemilik dari jalur snapshot internal[%s], karena regex[%s] tidak cocok dengan jalur snapshot", @@ -3514,6 +3516,7 @@ "ORG_ZSTACK_STORAGE_PRIMARY_BLOCK_10038": "gagal menemukan template volume berdasarkan name:%s", "ORG_ZSTACK_MEVOCO_10076": "volume(s) yang dapat dibagi [UUID: %s] terpasang; snapshot grup tidak didukung.", "ORG_ZSTACK_AI_MESSAGE_10002": "Tidak dapat membatalkan tugas pembuatan dataset karena resource UUID[%s] atau model center UUID[%s] adalah null", + "ORG_ZSTACK_AI_MESSAGE_10003": "Tugas unduhan model perlu dimulai ulang", "ORG_ZSTACK_AI_MESSAGE_10001": "Tidak dapat membatalkan tugas unduhan layanan model karena resource UUID[%s] atau model center UUID[%s] adalah null", "ORG_ZSTACK_IPSEC_VYOS_10009": "penerapan ke grup HA gagal karena %s", "ORG_ZSTACK_PREMIUM_EXTERNALSERVICE_PROMETHEUS_10013": "Gagal melakukan panggilan HTTP ke semua instance Prometheus di lingkungan cloud.", diff --git a/conf/i18n/globalErrorCodeMapping/global-error-ja-JP.json b/conf/i18n/globalErrorCodeMapping/global-error-ja-JP.json index 2924f658479..bf73aa503ff 100644 --- a/conf/i18n/globalErrorCodeMapping/global-error-ja-JP.json +++ b/conf/i18n/globalErrorCodeMapping/global-error-ja-JP.json @@ -36,9 +36,10 @@ "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_10051": "primaryStorageUuidの競合。ルートディスクオファリングによって指定されたプライマリストレージは%sであり、作成パラメータによって指定されたプライマリストレージは%sです。", "ORG_ZSTACK_V2V_10008": "ネットワーク[%s]で同じMACアドレス[%s]は許可されていません", "ORG_ZSTACK_V2V_10009": "ネットワーク[%s]でMACアドレス[%s]が重複しています", - "ORG_ZSTACK_STORAGE_PRIMARY_10052": "primaryStorageUuidの競合。ディスクオファリングによって指定されたプライマリストレージは%sであり、作成パラメータによって指定されたプライマリストレージは%sです。", + "ORG_ZSTACK_STORAGE_PRIMARY_10052": "primaryStorageUuidの競合。データディスクオファリングによって指定されたプライマリストレージは%sであり、作成パラメータによって指定されたプライマリストレージは%sです。", "ORG_ZSTACK_V2V_10006": "変換ホスト[uuid:%s]の基盤となるコンピューティングインスタンスはConnectedである必要があります", "ORG_ZSTACK_V2V_10007": "変換ホスト[uuid:%s]はプライマリストレージ[uuid:%s]への接続を確立できません", "ORG_ZSTACK_V2V_10004": "VMインスタンス[UUID:%s]が存在しないか、VMware VMインスタンスではありません", @@ -629,6 +630,7 @@ "ORG_ZSTACK_CONFIGURATION_10006": "サポートされていないインスタンスタイプ[%s]", "ORG_ZSTACK_MULTICAST_ROUTER_10007": "グループアドレス[%s]はマルチキャストグループアドレスではありません", "ORG_ZSTACK_STORAGE_ADDON_PRIMARY_10014": "ストレージは正常ではありません:%s", + "ORG_ZSTACK_STORAGE_ADDON_PRIMARY_10015": "優先タイプ%sに一致する利用可能なバックアップストレージがプライマリストレージ[uuid:%s]に存在しません", "ORG_ZSTACK_MULTICAST_ROUTER_10000": "マルチキャストルーター[uuid:%s]はVPCルーターに関連付けられていません", "ORG_ZSTACK_MULTICAST_ROUTER_10001": "マルチキャストは既にVPCルーターUUID[:%s]で有効化されています", "ORG_ZSTACK_STORAGE_ADDON_PRIMARY_10012": "内部スナップショットパス[%s]から所有ボリュームパスが見つかりません。正規表現[%s]がスナップショットパスに一致しません", @@ -3484,6 +3486,7 @@ "ORG_ZSTACK_STORAGE_PRIMARY_BLOCK_10038": "名前:%s のボリュームテンプレートが見つかりません", "ORG_ZSTACK_MEVOCO_10076": "共有可能ボリューム [UUID: %s] が接続されています。グループスナップショットはサポートされていません。", "ORG_ZSTACK_AI_MESSAGE_10002": "リソース UUID[%s] またはモデルセンター UUID[%s] が null であるため、データセット作成ジョブをキャンセルできません", + "ORG_ZSTACK_AI_MESSAGE_10003": "モデルダウンロードタスクを再開始する必要があります", "ORG_ZSTACK_AI_MESSAGE_10001": "リソース UUID[%s] またはモデルセンター UUID[%s] が null であるため、モデルサービスダウンロードタスクをキャンセルできません", "ORG_ZSTACK_IPSEC_VYOS_10009": "HA グループへの適用に失敗しました: %s", "ORG_ZSTACK_PREMIUM_EXTERNALSERVICE_PROMETHEUS_10013": "クラウド環境内のすべての Prometheus インスタンスへの HTTP 呼び出しに失敗しました。", diff --git a/conf/i18n/globalErrorCodeMapping/global-error-ko-KR.json b/conf/i18n/globalErrorCodeMapping/global-error-ko-KR.json index c73d9f274a1..d9323871208 100644 --- a/conf/i18n/globalErrorCodeMapping/global-error-ko-KR.json +++ b/conf/i18n/globalErrorCodeMapping/global-error-ko-KR.json @@ -36,9 +36,10 @@ "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_10051": "primaryStorageUuid 충돌: 루트 디스크 오퍼링에서 지정한 기본 스토리지가 %s이며, 생성 파라미터에서 지정한 기본 스토리지가 %s입니다", "ORG_ZSTACK_V2V_10008": "네트워크[%s]에서 동일한 MAC 주소 [%s]가 허용되지 않습니다", "ORG_ZSTACK_V2V_10009": "네트워크[%s]에서 중복된 MAC 주소 [%s]", - "ORG_ZSTACK_STORAGE_PRIMARY_10052": "primaryStorageUuid 충돌: 디스크 오퍼링에서 지정한 기본 스토리지가 %s이며, 생성 파라미터에서 지정한 기본 스토리지가 %s입니다", + "ORG_ZSTACK_STORAGE_PRIMARY_10052": "primaryStorageUuid 충돌: 데이터 디스크 오퍼링에서 지정한 기본 스토리지가 %s이며, 생성 파라미터에서 지정한 기본 스토리지가 %s입니다", "ORG_ZSTACK_V2V_10006": "변환 호스트[uuid:%s]의 기본 컴퓨팅 인스턴스가 Connected 상태여야 합니다", "ORG_ZSTACK_V2V_10007": "변환 호스트[uuid:%s]가 기본 스토리지[uuid:%s]에 연결을 수립할 수 없습니다", "ORG_ZSTACK_V2V_10004": "VM 인스턴스[UUID:%s]가 존재하지 않거나 VMware VM 인스턴스가 아닙니다", @@ -629,6 +630,7 @@ "ORG_ZSTACK_CONFIGURATION_10006": "지원되지 않는 인스턴스 유형[%s]", "ORG_ZSTACK_MULTICAST_ROUTER_10007": "그룹 주소 [%s]는 멀티캐스트 그룹 주소가 아닙니다", "ORG_ZSTACK_STORAGE_ADDON_PRIMARY_10014": "저장소가 정상 상태가 아닙니다:%s", + "ORG_ZSTACK_STORAGE_ADDON_PRIMARY_10015": "기본 저장소[uuid:%s]에 대해 선호하는 유형 %s의 사용 가능한 백업 저장소가 없습니다", "ORG_ZSTACK_MULTICAST_ROUTER_10000": "multicastRouter[uuid:%s]가 VPC 라우터와 연결되지 않았습니다", "ORG_ZSTACK_MULTICAST_ROUTER_10001": "VPC 라우터 UUID[:%s]에서 멀티캐스트가 이미 활성화되어 있습니다", "ORG_ZSTACK_STORAGE_ADDON_PRIMARY_10012": "정규식[%s]이 스냅샷 경로와 일치하지 않아 내부 스냅샷 경로[%s]에서 소유 볼륨 경로를 찾을 수 없습니다", @@ -3424,6 +3426,7 @@ "ORG_ZSTACK_STORAGE_PRIMARY_BLOCK_10038": "이름으로 볼륨 템플릿을 찾을 수 없습니다:%s", "ORG_ZSTACK_MEVOCO_10076": "공유 볼륨 [UUID: %s]이(가) 연결되어 있습니다. 그룹 스냅샷은 지원되지 않습니다.", "ORG_ZSTACK_AI_MESSAGE_10002": "리소스 UUID[%s] 또는 모델 센터 UUID[%s]가 null이므로 데이터세트 생성 작업을 취소할 수 없습니다", + "ORG_ZSTACK_AI_MESSAGE_10003": "모델 다운로드 작업을 다시 시작해야 합니다", "ORG_ZSTACK_AI_MESSAGE_10001": "리소스 UUID[%s] 또는 모델 센터 UUID[%s]가 null이므로 모델 서비스 다운로드 작업을 취소할 수 없습니다", "ORG_ZSTACK_IPSEC_VYOS_10009": "HA 그룹 적용 실패: %s", "ORG_ZSTACK_PREMIUM_EXTERNALSERVICE_PROMETHEUS_10013": "클라우드 환경의 모든 Prometheus 인스턴스에 HTTP 호출을 수행하지 못했습니다.", diff --git a/conf/i18n/globalErrorCodeMapping/global-error-ru-RU.json b/conf/i18n/globalErrorCodeMapping/global-error-ru-RU.json index 2454d7e5658..55bde5ee0c5 100644 --- a/conf/i18n/globalErrorCodeMapping/global-error-ru-RU.json +++ b/conf/i18n/globalErrorCodeMapping/global-error-ru-RU.json @@ -36,9 +36,10 @@ "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_10051": "конфликт primaryStorageUuid, основное хранилище, указанное в предложении корневого диска: %s, а основное хранилище, указанное в параметре создания: %s", "ORG_ZSTACK_V2V_10008": "Не допускается одинаковый MAC-адрес [%s] в сети[%s]", "ORG_ZSTACK_V2V_10009": "Дублирующийся MAC-адрес [%s] в сети[%s]", - "ORG_ZSTACK_STORAGE_PRIMARY_10052": "конфликт primaryStorageUuid, основное хранилище, указанное в предложении диска: %s, а основное хранилище, указанное в параметре создания: %s", + "ORG_ZSTACK_STORAGE_PRIMARY_10052": "конфликт primaryStorageUuid, основное хранилище, указанное в предложении диска данных: %s, а основное хранилище, указанное в параметре создания: %s", "ORG_ZSTACK_V2V_10006": "базовый вычислительный экземпляр хоста конвертации[uuid:%s] должен быть в состоянии Connected", "ORG_ZSTACK_V2V_10007": "хост конвертации[uuid:%s] не может установить соединение с основным хранилищем[uuid:%s]", "ORG_ZSTACK_V2V_10004": "Экземпляр ВМ [UUID:%s] не существует или не является экземпляром VMware VM", @@ -629,6 +630,7 @@ "ORG_ZSTACK_CONFIGURATION_10006": "неподдерживаемый тип экземпляра[%s]", "ORG_ZSTACK_MULTICAST_ROUTER_10007": "групповой адрес [%s] не является мультикаст-групповым адресом", "ORG_ZSTACK_STORAGE_ADDON_PRIMARY_10014": "хранилище нездорово:%s", + "ORG_ZSTACK_STORAGE_ADDON_PRIMARY_10015": "нет доступного резервного хранилища с предпочитаемыми типами %s для первичного хранилища[uuid:%s]", "ORG_ZSTACK_MULTICAST_ROUTER_10000": "multicastRouter[uuid:%s] не был связан с VPC-роутером", "ORG_ZSTACK_MULTICAST_ROUTER_10001": "multicast уже включен на VPC-роутере UUID[:%s]", "ORG_ZSTACK_STORAGE_ADDON_PRIMARY_10012": "невозможно найти путь к принадлежащему тому из внутреннего пути снимка[%s], так как регулярное выражение[%s] не соответствует пути снимка", @@ -3514,6 +3516,7 @@ "ORG_ZSTACK_STORAGE_PRIMARY_BLOCK_10038": "не удалось найти шаблон тома по имени:%s", "ORG_ZSTACK_MEVOCO_10076": "Общие тома [UUID: %s] подключены; групповой снапшот не поддерживается.", "ORG_ZSTACK_AI_MESSAGE_10002": "Невозможно отменить задачу создания набора данных, так как ресурс UUID[%s] или UUID центра моделей[%s] имеет значение null", + "ORG_ZSTACK_AI_MESSAGE_10003": "Задача загрузки модели должна быть перезапущена", "ORG_ZSTACK_AI_MESSAGE_10001": "Невозможно отменить задачу загрузки сервиса модели, так как ресурс UUID[%s] или UUID центра моделей[%s] имеет значение null", "ORG_ZSTACK_IPSEC_VYOS_10009": "применение к группе HA не удалось из-за %s", "ORG_ZSTACK_PREMIUM_EXTERNALSERVICE_PROMETHEUS_10013": "Не удалось выполнить HTTP-вызовы ко всем экземплярам Prometheus в облачной среде.", diff --git a/conf/i18n/globalErrorCodeMapping/global-error-th-TH.json b/conf/i18n/globalErrorCodeMapping/global-error-th-TH.json index 9db32b34264..3118556b6d7 100644 --- a/conf/i18n/globalErrorCodeMapping/global-error-th-TH.json +++ b/conf/i18n/globalErrorCodeMapping/global-error-th-TH.json @@ -36,9 +36,10 @@ "ORG_ZSTACK_STORAGE_PRIMARY_10044": "ไม่พบ primary storage ที่มีคุณสมบัติเหมาะสม; ข้อผิดพลาดคือ %s", "ORG_ZSTACK_NETWORK_SERVICE_NFVINSTGROUP_10000": "ไม่สามารถอัปเดตสถานะกลุ่ม: %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]", - "ORG_ZSTACK_STORAGE_PRIMARY_10052": "ความขัดแย้งของ primaryStorageUuid: primary storage ที่ระบุโดย disk offering คือ %s ในขณะที่ primary storage ที่ระบุในพารามิเตอร์การสร้างคือ %s", + "ORG_ZSTACK_STORAGE_PRIMARY_10052": "ความขัดแย้งของ primaryStorageUuid: primary storage ที่ระบุโดย data disk offering คือ %s ในขณะที่ primary storage ที่ระบุในพารามิเตอร์การสร้างคือ %s", "ORG_ZSTACK_V2V_10006": "compute instance พื้นฐานของ conversion host[uuid:%s] ควรอยู่ในสถานะ Connected", "ORG_ZSTACK_V2V_10007": "conversion host[uuid:%s] ไม่สามารถสร้างการเชื่อมต่อกับ primary storage[uuid:%s]", "ORG_ZSTACK_V2V_10004": "VM instance [UUID:%s] ไม่มีอยู่หรือไม่ใช่ VMware VM instance", @@ -629,6 +630,7 @@ "ORG_ZSTACK_CONFIGURATION_10006": "instance type[%s] ไม่รองรับ", "ORG_ZSTACK_MULTICAST_ROUTER_10007": "group address [%s] ไม่ใช่ multicast group address", "ORG_ZSTACK_STORAGE_ADDON_PRIMARY_10014": "storage ไม่พร้อมใช้งาน:%s", + "ORG_ZSTACK_STORAGE_ADDON_PRIMARY_10015": "ไม่มี backup storage ที่ใช้ได้ซึ่งมีประเภทที่ต้องการ %s สำหรับ primary storage[uuid:%s]", "ORG_ZSTACK_MULTICAST_ROUTER_10000": "multicastRouter[uuid:%s] ไม่ได้เชื่อมโยงกับ VPC router", "ORG_ZSTACK_MULTICAST_ROUTER_10001": "multicast เปิดใช้งานอยู่แล้วบน VPC router UUID[:%s]", "ORG_ZSTACK_STORAGE_ADDON_PRIMARY_10012": "ไม่พบ path ของ volume ที่เป็นเจ้าของจาก internal snapshot path[%s] เนื่องจาก regex[%s] ไม่สามารถจับคู่กับ snapshot path ได้", @@ -3514,6 +3516,7 @@ "ORG_ZSTACK_STORAGE_PRIMARY_BLOCK_10038": "ไม่สามารถค้นหา volume template ตามชื่อ:%s", "ORG_ZSTACK_MEVOCO_10076": "shareable volume(s) [UUID: %s] ถูกแนบแล้ว; group snapshot ไม่รองรับ", "ORG_ZSTACK_AI_MESSAGE_10002": "ไม่สามารถยกเลิกงานสร้าง dataset ได้เนื่องจาก resource UUID[%s] หรือ model center UUID[%s] เป็น null", + "ORG_ZSTACK_AI_MESSAGE_10003": "งาน download ของ model จำเป็นต้องเริ่มใหม่", "ORG_ZSTACK_AI_MESSAGE_10001": "ไม่สามารถยกเลิกงาน download ของ model service ได้เนื่องจาก resource UUID[%s] หรือ model center UUID[%s] เป็น null", "ORG_ZSTACK_IPSEC_VYOS_10009": "การ apply ไปยัง HA group ล้มเหลวเนื่องจาก %s", "ORG_ZSTACK_PREMIUM_EXTERNALSERVICE_PROMETHEUS_10013": "ไม่สามารถทำ HTTP calls ไปยัง Prometheus instances ทั้งหมดใน cloud environment ได้", diff --git a/conf/i18n/globalErrorCodeMapping/global-error-zh_CN.json b/conf/i18n/globalErrorCodeMapping/global-error-zh_CN.json index 84609838ddc..a6a01a7abe3 100644 --- a/conf/i18n/globalErrorCodeMapping/global-error-zh_CN.json +++ b/conf/i18n/globalErrorCodeMapping/global-error-zh_CN.json @@ -29,16 +29,17 @@ "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", "ORG_ZSTACK_STORAGE_PRIMARY_10044": "无法找到任何合格的主存储,错误信息是 %s", "ORG_ZSTACK_NETWORK_SERVICE_NFVINSTGROUP_10000": "失败更新组状态: %s", - "ORG_ZSTACK_STORAGE_PRIMARY_10050": "主存储UUID冲突,实例报价中指定的主存储为%s,创建参数中指定的主存储为%s", + "ORG_ZSTACK_STORAGE_PRIMARY_10050": "主存储UUID冲突,计算规格指定的主存储为%s,而创建参数中指定的主存储为%s", + "ORG_ZSTACK_STORAGE_PRIMARY_10051": "主存储UUID冲突,系统云盘规格指定的主存储为%s,而创建参数中指定的主存储为%s", "ORG_ZSTACK_V2V_10008": "不允许在网络[%s]中使用相同的MAC地址[%s]", "ORG_ZSTACK_V2V_10009": "重复的 MAC 地址 [%s] 在网络[%s] 中", - "ORG_ZSTACK_STORAGE_PRIMARY_10052": "主存储UUID冲突,由磁盘配置指定的主存储为%s,而创建参数中指定的主存储为%s", + "ORG_ZSTACK_STORAGE_PRIMARY_10052": "主存储UUID冲突,数据云盘规格指定的主存储为%s,而创建参数中指定的主存储为%s", "ORG_ZSTACK_V2V_10006": "转换主机[uuid:%s]的底层宿主机应当是已连接状态", "ORG_ZSTACK_V2V_10007": "转换主机[uuid:%s]无法连接到主要存储[uuid:%s]", "ORG_ZSTACK_V2V_10004": "vm 实例[uuid:%s] 不存在或不是一个 VMware 虚拟机", @@ -629,6 +630,7 @@ "ORG_ZSTACK_CONFIGURATION_10006": "不支持的实例配置类型[%s]", "ORG_ZSTACK_MULTICAST_ROUTER_10007": "组地址 [%s] 不是多播地址", "ORG_ZSTACK_STORAGE_ADDON_PRIMARY_10014": "存储不健康:%s", + "ORG_ZSTACK_STORAGE_ADDON_PRIMARY_10015": "没有可用的镜像服务器匹配首选类型 %s,主存储[uuid:%s]", "ORG_ZSTACK_MULTICAST_ROUTER_10000": "多播路由器[uuid:%s]尚未绑定到VPC路由器", "ORG_ZSTACK_MULTICAST_ROUTER_10001": "多播已在VPC路由器uuid[:%s]上启用", "ORG_ZSTACK_STORAGE_ADDON_PRIMARY_10012": "无法从内部快照路径[%s]找到所属卷路径,因为正则表达式[%s]与快照路径不匹配", @@ -3374,7 +3376,7 @@ "ORG_ZSTACK_NETWORK_HUAWEI_IMASTER_10019": "删除 SDN 控制器 [IP:%s] 的令牌失败,因为 %s", "ORG_ZSTACK_STORAGE_PRIMARY_BLOCK_10004": "无法执行映射LUN到主机流程,无效的LUN ID", "ORG_ZSTACK_NETWORK_SERVICE_PORTFORWARDING_10007": "端口转发规则 rule[uuid:%s] 没有绑定到任何 VM 的网卡上,无法解除绑定", - "ORG_ZSTACK_MEVOCO_10088": "无法为挂载状态以外的卷[%s]创建快照", + "ORG_ZSTACK_MEVOCO_10088": "无法为云盘[uuid:%s]创建快照,因为该云盘未挂载到任何云主机。请先将云盘挂载到云主机后再创建快照。相关云盘: %s", "ORG_ZSTACK_STORAGE_PRIMARY_BLOCK_10005": "无法执行映射LUN到主机流程,无效的LUN类型", "ORG_ZSTACK_NETWORK_SERVICE_PORTFORWARDING_10008": "端口转发规则[uuid:%s]已绑定到VM网卡[uuid:%s],无法再次绑定", "ORG_ZSTACK_MEVOCO_10087": "如何一个运行中的VM[uuid:%s]没有宿主机uuid?", @@ -3515,6 +3517,7 @@ "ORG_ZSTACK_STORAGE_PRIMARY_BLOCK_10038": "未能根据名称找到模板 LUN:%s", "ORG_ZSTACK_MEVOCO_10076": "可共享的卷[uuid: %s]已挂载,不支持组快照。", "ORG_ZSTACK_AI_MESSAGE_10002": "无法取消数据集创建任务,因为 resourceUuid[%s] 或 modelCenterUuid[%s] 为空", + "ORG_ZSTACK_AI_MESSAGE_10003": "模型下载任务需重新下载", "ORG_ZSTACK_AI_MESSAGE_10001": "无法取消模型服务下载任务,因为 resourceUuid[%s] 或 modelCenterUuid[%s] 为空", "ORG_ZSTACK_IPSEC_VYOS_10009": "应用到高可用组失败,因为 %s", "ORG_ZSTACK_PREMIUM_EXTERNALSERVICE_PROMETHEUS_10013": "失败的 HTTP 调用所有 Prometheus 实例", diff --git a/conf/i18n/globalErrorCodeMapping/global-error-zh_TW.json b/conf/i18n/globalErrorCodeMapping/global-error-zh_TW.json index 35f1b8c8443..b696d46d059 100644 --- a/conf/i18n/globalErrorCodeMapping/global-error-zh_TW.json +++ b/conf/i18n/globalErrorCodeMapping/global-error-zh_TW.json @@ -29,16 +29,17 @@ "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", "ORG_ZSTACK_STORAGE_PRIMARY_10044": "無法找到任何合格的主儲儲,錯誤誤信息是 %s", "ORG_ZSTACK_NETWORK_SERVICE_NFVINSTGROUP_10000": "失敗更新组状态: %s", - "ORG_ZSTACK_STORAGE_PRIMARY_10050": "主儲儲UUID冲突,实例報价中指定的主儲儲为%s,創建參數中指定的主儲儲为%s", + "ORG_ZSTACK_STORAGE_PRIMARY_10050": "主儲存UUID衝突,計算規格指定的主儲存為%s,而創建參數中指定的主儲存為%s", + "ORG_ZSTACK_STORAGE_PRIMARY_10051": "主儲存UUID衝突,系統雲盤規格指定的主儲存為%s,而創建參數中指定的主儲存為%s", "ORG_ZSTACK_V2V_10008": "不允許在网络[%s]中使用相同的MAC地址[%s]", "ORG_ZSTACK_V2V_10009": "重复的 MAC 地址 [%s] 在网络[%s] 中", - "ORG_ZSTACK_STORAGE_PRIMARY_10052": "主儲儲UUID冲突,由磁碟配置指定的主儲儲为%s,而創建參數中指定的主儲儲为%s", + "ORG_ZSTACK_STORAGE_PRIMARY_10052": "主儲存UUID衝突,數據雲盤規格指定的主儲存為%s,而創建參數中指定的主儲存為%s", "ORG_ZSTACK_V2V_10006": "转换主機[uuid:%s]的底层宿主機应當是已連接状态", "ORG_ZSTACK_V2V_10007": "转换主機[uuid:%s]無法連接到主要儲儲[uuid:%s]", "ORG_ZSTACK_V2V_10004": "vm 实例[uuid:%s] 不儲在或不是一個 VMware 虚拟機", @@ -629,6 +630,7 @@ "ORG_ZSTACK_CONFIGURATION_10006": "不支持的实例配置類型[%s]", "ORG_ZSTACK_MULTICAST_ROUTER_10007": "组地址 [%s] 不是多播地址", "ORG_ZSTACK_STORAGE_ADDON_PRIMARY_10014": "儲儲不健康:%s", + "ORG_ZSTACK_STORAGE_ADDON_PRIMARY_10015": "沒有可用的鏡像服務器匹配首選類型 %s,主儲存[uuid:%s]", "ORG_ZSTACK_MULTICAST_ROUTER_10000": "多播路由器[uuid:%s]尚未綁定到VPC路由器", "ORG_ZSTACK_MULTICAST_ROUTER_10001": "多播已在VPC路由器uuid[:%s]上啟用", "ORG_ZSTACK_STORAGE_ADDON_PRIMARY_10012": "無法从内部快照路径[%s]找到所属卷路径,因为正则表達式[%s]与快照路径不匹配", @@ -3514,6 +3516,7 @@ "ORG_ZSTACK_STORAGE_PRIMARY_BLOCK_10038": "未能根据名称找到模板 LUN:%s", "ORG_ZSTACK_MEVOCO_10076": "可共享的卷[uuid: %s]已挂载,不支持组快照。", "ORG_ZSTACK_AI_MESSAGE_10002": "無法取消數据叢創建任務,因为 resourceUuid[%s] 或 modelCenterUuid[%s] 为空", + "ORG_ZSTACK_AI_MESSAGE_10003": "模型下載任務需重新下載", "ORG_ZSTACK_AI_MESSAGE_10001": "無法取消模型服務下载任務,因为 resourceUuid[%s] 或 modelCenterUuid[%s] 为空", "ORG_ZSTACK_IPSEC_VYOS_10009": "应用到高可用组失敗,因为 %s", "ORG_ZSTACK_PREMIUM_EXTERNALSERVICE_PROMETHEUS_10013": "失敗的 HTTP 調用所有 Prometheus 实例", 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 0e32b390993..97b03278a02 100755 --- a/conf/springConfigXml/DatabaseFacade.xml +++ b/conf/springConfigXml/DatabaseFacade.xml @@ -87,7 +87,8 @@ sync - true + true + 50 diff --git a/conf/springConfigXml/Error.xml b/conf/springConfigXml/Error.xml index a8855a32487..ed5261f8d58 100755 --- a/conf/springConfigXml/Error.xml +++ b/conf/springConfigXml/Error.xml @@ -13,4 +13,9 @@ default-init-method="init" default-destroy-method="destroy"> + + + + + diff --git a/conf/tools/install.sh b/conf/tools/install.sh index 2821bc9ab6d..8a759869850 100755 --- a/conf/tools/install.sh +++ b/conf/tools/install.sh @@ -10,7 +10,7 @@ fi pypi_path=file://$cwd/../../../static/pypi/simple usage() { - echo "usage:$0 [zstack-cli|zstack-ctl|zstack-dashboard|zstack-ui]" + echo "usage:$0 [zstack-cli|zstack-ctl|zstack-dashboard|zstack-ui|zstack-sys]" exit 1 } @@ -22,29 +22,38 @@ if [ -z $tool ]; then fi install_pip() { - pip --version | grep 7.0.3 >/dev/null || easy_install -i $pypi_path --upgrade pip + pip3.11 --version | grep 22.3.1 >/dev/null || yum install -y python3.11-pip } -install_virtualenv() { - virtualenv --version | grep 12.1.1 >/dev/null || pip install -i $pypi_path --ignore-installed virtualenv==12.1.1 +# Ensure the virtualenv at $1 is a Python 3.11 venv. +# If it does not exist or is a legacy Python 2 venv, recreate it. +ensure_python3_venv() { + local venv_path=$1 + local allowed_prefix="/var/lib/zstack/virtualenv" + + if [[ "$venv_path" != "$allowed_prefix"* || "$venv_path" == *".."* ]]; then + echo "Error: Path must start with $allowed_prefix. Provided: $venv_path" >&2 + exit 1 + fi + + if [ -d "$venv_path" ] && [ -x "$venv_path/bin/python3.11" ]; then + return 0 + fi + # 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 } + cd $cwd install_pip -install_virtualenv cd /tmp if [ $tool = 'zstack-cli' ]; then CLI_VIRENV_PATH=/var/lib/zstack/virtualenv/zstackcli [ ! -z $force ] && rm -rf $CLI_VIRENV_PATH - if [ ! -d "$CLI_VIRENV_PATH" ]; then - virtualenv $CLI_VIRENV_PATH --python=python2.7 - if [ $? -ne 0 ]; then - rm -rf $CLI_VIRENV_PATH - exit 1 - fi - fi + ensure_python3_venv "$CLI_VIRENV_PATH" . $CLI_VIRENV_PATH/bin/activate cd $cwd pip install -i $pypi_path --trusted-host localhost --ignore-installed zstackcli-*.tar.gz apibinding-*.tar.gz @@ -70,36 +79,30 @@ if [ $tool = 'zstack-cli' ]; then exit 1 fi fi + [ -f $CLI_VIRENV_PATH/bin/zstack-cli ] && cp $CLI_VIRENV_PATH/bin/zstack-cli /usr/bin/zstack-cli chmod +x /usr/bin/zstack-cli elif [ $tool = 'zstack-ctl' ]; then CTL_VIRENV_PATH=/var/lib/zstack/virtualenv/zstackctl - rm -rf $CTL_VIRENV_PATH && virtualenv $CTL_VIRENV_PATH --python=python2.7 || exit 1 + ensure_python3_venv "$CTL_VIRENV_PATH" . $CTL_VIRENV_PATH/bin/activate cd $cwd TMPDIR=/usr/local/zstack/ pip install -i $pypi_path --trusted-host localhost --ignore-installed zstackctl-*.tar.gz || exit 1 - TMPDIR=/usr/local/zstack/ pip install -i $pypi_path --trusted-host localhost --ignore-installed pycrypto==2.6.1 || exit 1 + TMPDIR=/usr/local/zstack/ pip install -i $pypi_path --trusted-host localhost --ignore-installed pycryptodome || exit 1 + [ -f $CTL_VIRENV_PATH/bin/zstack-ctl ] && cp $CTL_VIRENV_PATH/bin/zstack-ctl /usr/bin/zstack-ctl chmod +x /usr/bin/zstack-ctl - python $CTL_VIRENV_PATH/lib/python2.7/site-packages/zstackctl/generate_zstackctl_bash_completion.py + python $CTL_VIRENV_PATH/lib/python3.11/site-packages/zstackctl/generate_zstackctl_bash_completion.py elif [ $tool = 'zstack-sys' ]; then SYS_VIRENV_PATH=/var/lib/zstack/virtualenv/zstacksys - NEED_INSTALL=false - if [ -d $SYS_VIRENV_PATH ]; then - . $SYS_VIRENV_PATH/bin/activate - if ! ansible --version | grep -q 'core 2.11.12.3'; then - deactivate - NEED_INSTALL=true - fi - else - NEED_INSTALL=true - fi - if $NEED_INSTALL; then - rm -rf $SYS_VIRENV_PATH && virtualenv $SYS_VIRENV_PATH --python=python2.7 || exit 1 + ensure_python3_venv "$SYS_VIRENV_PATH" + # 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 - TMPDIR=/usr/local/zstack/ pip install -i $pypi_path --trusted-host localhost --ignore-installed setuptools==39.2.0 || exit 1 - TMPDIR=/usr/local/zstack/ pip install -i $pypi_path --trusted-host localhost --ignore-installed ansible==4.10.0 || exit 1 + #TMPDIR=/usr/local/zstack/ pip install -i $pypi_path --trusted-host localhost --ignore-installed setuptools==65.5.1 || exit 1 + TMPDIR=/usr/local/zstack/ pip install -i $pypi_path --trusted-host localhost --ignore-installed ansible==9.13.0 || exit 1 cat > /usr/bin/ansible << EOF #! /bin/sh @@ -114,7 +117,7 @@ LC_ALL=en_US.utf8 export LANG LC_ALL . ${VIRTUAL_ENV}/bin/activate -ansible \$@ +\${VIRTUAL_ENV}/bin/ansible \$@ EOF chmod +x /usr/bin/ansible @@ -131,7 +134,7 @@ LC_ALL=en_US.utf8 export LANG LC_ALL . ${VIRTUAL_ENV}/bin/activate -ansible-playbook \$@ +\${VIRTUAL_ENV}/bin/ansible-playbook \$@ EOF chmod +x /usr/bin/ansible-playbook fi @@ -140,7 +143,7 @@ elif [ $tool = 'zstack-dashboard' ]; then UI_VIRENV_PATH=/var/lib/zstack/virtualenv/zstack-dashboard [ ! -z $force ] && rm -rf $UI_VIRENV_PATH if [ ! -d "$UI_VIRENV_PATH" ]; then - virtualenv $UI_VIRENV_PATH --python=python2.7 + python3.11 -m venv $UI_VIRENV_PATH if [ $? -ne 0 ]; then rm -rf $UI_VIRENV_PATH exit 1 diff --git a/core/pom.xml b/core/pom.xml index 0ab8bb020f0..484983dba03 100644 --- a/core/pom.xml +++ b/core/pom.xml @@ -167,11 +167,6 @@ io.opentelemetry.semconv opentelemetry-semconv - - - io.opentelemetry - opentelemetry-semconv - io.opentelemetry opentelemetry-sdk-metrics diff --git a/core/src/main/java/org/zstack/core/CoreGlobalProperty.java b/core/src/main/java/org/zstack/core/CoreGlobalProperty.java index 393face30d3..828b06b0bf2 100755 --- a/core/src/main/java/org/zstack/core/CoreGlobalProperty.java +++ b/core/src/main/java/org/zstack/core/CoreGlobalProperty.java @@ -12,6 +12,8 @@ public class CoreGlobalProperty { @GlobalProperty(name = "unitTestOn", defaultValue = "false") @AvailableValues(value ={"true","false"}) public static boolean UNIT_TEST_ON; + @GlobalProperty(name = "elaboration.slowThresholdMs", defaultValue = "0") + public static long ELABORATION_SLOW_THRESHOLD_MS; @GlobalProperty(name = "beanRefContextConf", defaultValue = "beanRefContext.xml") public static String BEAN_REF_CONTEXT_CONF; @GlobalProperty(name = "beanConf", defaultValue = "zstack.xml") diff --git a/core/src/main/java/org/zstack/core/Platform.java b/core/src/main/java/org/zstack/core/Platform.java index 5cff425e3da..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; @@ -936,12 +937,15 @@ public static boolean killProcess(int pid, Integer timeout) { } } + private static volatile boolean slowElaborationWired = false; + private static ErrorCodeElaboration elaborate(String fmt, Object...args) { - try { - if (String.format(fmt, args).length() > StringSimilarity.maxElaborationRegex) { - return null; - } + if (!slowElaborationWired) { + StringSimilarity.slowElaborationThresholdMs = CoreGlobalProperty.ELABORATION_SLOW_THRESHOLD_MS; + slowElaborationWired = true; + } + try { ErrorCodeElaboration elaboration = StringSimilarity.findSimilar(fmt, args); if (elaboration == null) { return null; @@ -979,6 +983,28 @@ public static ErrorCode err(String globalErrorCode, Enum errCode, ErrorCode caus handleErrorElaboration(errCode, fmt, result, cause, args); addErrorCounter(result); result.setGlobalErrorCode(globalErrorCode); + if (args != null && args.length > 0) { + result.setFormatArgs(java.util.Arrays.stream(args) + .map(a -> a == null ? "null" : a.toString()) + .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/AnsibleFacadeImpl.java b/core/src/main/java/org/zstack/core/ansible/AnsibleFacadeImpl.java index d9e40575833..dada8634e59 100755 --- a/core/src/main/java/org/zstack/core/ansible/AnsibleFacadeImpl.java +++ b/core/src/main/java/org/zstack/core/ansible/AnsibleFacadeImpl.java @@ -136,7 +136,7 @@ void init() { "NEED_INSTALL=false; " + "if [ -d /var/lib/zstack/virtualenv/zstacksys ]; then " + ". /var/lib/zstack/virtualenv/zstacksys/bin/activate; " + - "if ! ansible --version | grep -q 'core 2.11.12'; then " + + "if ! ansible --version | grep -q 'core 2.16.14'; then " + "deactivate; " + "NEED_INSTALL=true; " + "fi; " + @@ -144,10 +144,10 @@ void init() { "NEED_INSTALL=true; "+ "fi; " + "if $NEED_INSTALL; then " + - "sudo bash -c 'rm -rf /var/lib/zstack/virtualenv/zstacksys && virtualenv /var/lib/zstack/virtualenv/zstacksys --python=python2.7; "+ + "sudo bash -c 'rm -rf /var/lib/zstack/virtualenv/zstacksys && python3.11 -m venv /var/lib/zstack/virtualenv/zstacksys; "+ ". /var/lib/zstack/virtualenv/zstacksys/bin/activate; "+ - "TMPDIR=/usr/local/zstack/ pip install -i file://%s --trusted-host localhost -I setuptools==39.2.0; "+ - "TMPDIR=/usr/local/zstack/ pip install -i file://%s --trusted-host localhost -I ansible==4.10.0'; "+ + "TMPDIR=/usr/local/zstack/ pip install -i file://%s --trusted-host localhost -I setuptools==65.5.1; "+ + "TMPDIR=/usr/local/zstack/ pip install -i file://%s --trusted-host localhost -I ansible==9.13.0'; "+ "fi" , AnsibleConstant.PYPI_REPO, AnsibleConstant.PYPI_REPO), false); @@ -252,19 +252,20 @@ private void run(Completion completion) { Map arguments = collectArguments(msg); logger.debug(String.format("start running ansible for playbook[%s]", msg.getPlayBookPath())); String executable = msg.getAnsibleExecutable() == null ? AnsibleGlobalProperty.EXECUTABLE : msg.getAnsibleExecutable(); + long timeout = TimeUnit.MILLISECONDS.toSeconds(msg.getTimeout()); try { String output; if (AnsibleGlobalProperty.DEBUG_MODE2) { - output = ShellUtils.run(String.format("bash -c '. /var/lib/zstack/virtualenv/zstacksys/bin/activate; PYTHONPATH=%s timeout %d %s %s -i %s -vvvv --private-key %s -e '\\''%s'\\' | tee -a %s", + output = ShellUtils.run(String.format("bash -c '. /var/lib/zstack/virtualenv/zstacksys/bin/activate; PYTHONPATH=%s timeout %d %s -B %s -i %s -vvvv --private-key %s -e '\\''%s'\\' | tee -a %s", AnsibleConstant.ZSTACKLIB_ROOT, timeout, executable, playBookPath, AnsibleConstant.INVENTORY_FILE, msg.getPrivateKeyFile(), JSONObjectUtil.dumpPretty(arguments), AnsibleConstant.LOG_PATH), AnsibleConstant.ROOT_DIR); } else if (AnsibleGlobalProperty.DEBUG_MODE) { - output = ShellUtils.run(String.format("bash -c '. /var/lib/zstack/virtualenv/zstacksys/bin/activate; PYTHONPATH=%s timeout %d %s %s -i %s -vvvv --private-key %s -e '\\''%s'\\'", + output = ShellUtils.run(String.format("bash -c '. /var/lib/zstack/virtualenv/zstacksys/bin/activate; PYTHONPATH=%s timeout %d %s -B %s -i %s -vvvv --private-key %s -e '\\''%s'\\'", AnsibleConstant.ZSTACKLIB_ROOT, timeout, executable, playBookPath, AnsibleConstant.INVENTORY_FILE, msg.getPrivateKeyFile(), JSONObjectUtil.dumpPretty(arguments)), AnsibleConstant.ROOT_DIR); } else { - output = ShellUtils.run(String.format("bash -c '. /var/lib/zstack/virtualenv/zstacksys/bin/activate; PYTHONPATH=%s timeout %d %s %s -i %s --private-key %s -e '\\''%s'\\'", + output = ShellUtils.run(String.format("bash -c '. /var/lib/zstack/virtualenv/zstacksys/bin/activate; PYTHONPATH=%s timeout %d %s -B %s -i %s --private-key %s -e '\\''%s'\\'", AnsibleConstant.ZSTACKLIB_ROOT, timeout, executable, playBookPath, AnsibleConstant.INVENTORY_FILE, msg.getPrivateKeyFile(), JSONObjectUtil.dumpPretty(arguments)), AnsibleConstant.ROOT_DIR); } 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/cloudbus/ResourceDestinationMakerImpl.java b/core/src/main/java/org/zstack/core/cloudbus/ResourceDestinationMakerImpl.java index 08a776f1db2..9ead578395d 100755 --- a/core/src/main/java/org/zstack/core/cloudbus/ResourceDestinationMakerImpl.java +++ b/core/src/main/java/org/zstack/core/cloudbus/ResourceDestinationMakerImpl.java @@ -27,27 +27,27 @@ public class ResourceDestinationMakerImpl implements ManagementNodeChangeListene private DatabaseFacade dbf; @Override - public void nodeJoin(ManagementNodeInventory inv) { + public synchronized void nodeJoin(ManagementNodeInventory inv) { nodeHash.add(inv.getUuid()); nodes.put(inv.getUuid(), new NodeInfo(inv)); } @Override - public void nodeLeft(ManagementNodeInventory inv) { + public synchronized void nodeLeft(ManagementNodeInventory inv) { String nodeId = inv.getUuid(); nodeHash.remove(nodeId); nodes.remove(nodeId); } @Override - public void iAmDead(ManagementNodeInventory inv) { + public synchronized void iAmDead(ManagementNodeInventory inv) { String nodeId = inv.getUuid(); nodeHash.remove(nodeId); nodes.remove(nodeId); } @Override - public void iJoin(ManagementNodeInventory inv) { + public synchronized void iJoin(ManagementNodeInventory inv) { List lst = Q.New(ManagementNodeVO.class).list(); lst.forEach((ManagementNodeVO node) -> { nodeHash.add(node.getUuid()); @@ -56,7 +56,7 @@ public void iJoin(ManagementNodeInventory inv) { } @Override - public String makeDestination(String resourceUuid) { + public synchronized String makeDestination(String resourceUuid) { String nodeUuid = nodeHash.get(resourceUuid); if (nodeUuid == null) { throw new CloudRuntimeException("Cannot find any available management node to send message"); @@ -66,18 +66,18 @@ public String makeDestination(String resourceUuid) { } @Override - public boolean isManagedByUs(String resourceUuid) { + public synchronized boolean isManagedByUs(String resourceUuid) { String nodeUuid = makeDestination(resourceUuid); return nodeUuid.equals(Platform.getManagementServerId()); } @Override - public Collection getManagementNodesInHashRing() { - return nodeHash.getNodes(); + public synchronized Collection getManagementNodesInHashRing() { + return new ArrayList<>(nodeHash.getNodes()); } @Override - public NodeInfo getNodeInfo(String nodeUuid) { + public synchronized NodeInfo getNodeInfo(String nodeUuid) { NodeInfo info = nodes.get(nodeUuid); if (info == null) { ManagementNodeVO vo = dbf.findByUuid(nodeUuid, ManagementNodeVO.class); @@ -93,17 +93,17 @@ public NodeInfo getNodeInfo(String nodeUuid) { } @Override - public Collection getAllNodeInfo() { - return nodes.values(); + public synchronized Collection getAllNodeInfo() { + return new ArrayList<>(nodes.values()); } @Override - public int getManagementNodeCount() { - return nodes.values().size(); + public synchronized int getManagementNodeCount() { + return nodes.size(); } - public boolean isNodeInCircle(String nodeId) { + public synchronized boolean isNodeInCircle(String nodeId) { return nodeHash.hasNode(nodeId); } } 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/db/UpdateQueryImpl.java b/core/src/main/java/org/zstack/core/db/UpdateQueryImpl.java index 6475b903c6e..b30adb194b5 100755 --- a/core/src/main/java/org/zstack/core/db/UpdateQueryImpl.java +++ b/core/src/main/java/org/zstack/core/db/UpdateQueryImpl.java @@ -54,7 +54,8 @@ public UpdateQuery set(SingularAttribute attr, Object val) { @Override public UpdateQuery condAnd(SingularAttribute attr, Op op, Object val) { if ((op == Op.IN || op == Op.NOT_IN) && !(val instanceof Collection)) { - throw new CloudRuntimeException(String.format("for operation IN or NOT IN, a Collection value is expected, but %s got", val.getClass())); + throw new CloudRuntimeException(String.format("for operation IN or NOT IN, a Collection value is expected, but %s got", + val == null ? "null" : val.getClass())); } Cond cond = new Cond(); diff --git a/core/src/main/java/org/zstack/core/errorcode/GlobalErrorCodeI18nService.java b/core/src/main/java/org/zstack/core/errorcode/GlobalErrorCodeI18nService.java new file mode 100644 index 00000000000..52c23a0e088 --- /dev/null +++ b/core/src/main/java/org/zstack/core/errorcode/GlobalErrorCodeI18nService.java @@ -0,0 +1,29 @@ +package org.zstack.core.errorcode; + +import org.zstack.header.errorcode.ErrorCode; + +public interface GlobalErrorCodeI18nService { + /** + * Get localized message for a globalErrorCode. + * + * @param globalErrorCode the global error code key + * @param locale the locale key (e.g. "zh_CN", "ja-JP") + * @param formatArgs optional format arguments for %s placeholders + * @return the localized message, or null if not found + */ + String getLocalizedMessage(String globalErrorCode, String locale, String[] formatArgs); + + /** + * Recursively localize an ErrorCode and its cause chain, + * setting the message field on each ErrorCode. + * + * @param error the ErrorCode to localize + * @param locale the locale key + */ + void localizeErrorCode(ErrorCode error, String locale); + + /** + * Get the set of available locale keys loaded from JSON files. + */ + java.util.Set getAvailableLocales(); +} diff --git a/core/src/main/java/org/zstack/core/errorcode/GlobalErrorCodeI18nServiceImpl.java b/core/src/main/java/org/zstack/core/errorcode/GlobalErrorCodeI18nServiceImpl.java new file mode 100644 index 00000000000..b6820fc2d3b --- /dev/null +++ b/core/src/main/java/org/zstack/core/errorcode/GlobalErrorCodeI18nServiceImpl.java @@ -0,0 +1,160 @@ +package org.zstack.core.errorcode; + +import org.zstack.header.Component; +import org.zstack.header.errorcode.ErrorCode; +import org.zstack.header.errorcode.ErrorCodeList; +import org.zstack.utils.Utils; +import org.zstack.utils.gson.JSONObjectUtil; +import org.zstack.utils.logging.CLogger; +import org.zstack.utils.path.PathUtil; + +import java.io.File; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.util.*; +import java.util.concurrent.ConcurrentHashMap; + +public class GlobalErrorCodeI18nServiceImpl implements GlobalErrorCodeI18nService, Component { + private static final CLogger logger = Utils.getLogger(GlobalErrorCodeI18nServiceImpl.class); + + private static final String I18N_FOLDER = "i18n" + File.separator + "globalErrorCodeMapping"; + private static final String FILE_PREFIX = "global-error-"; + private static final String FILE_SUFFIX = ".json"; + + // locale -> (globalErrorCode -> template) + private final Map> localeMessages = new ConcurrentHashMap<>(); + + @Override + public boolean start() { + loadAllJsonFiles(); + return true; + } + + @Override + public boolean stop() { + return true; + } + + private void loadAllJsonFiles() { + try { + List paths = PathUtil.scanFolderOnClassPath(I18N_FOLDER); + for (String path : paths) { + if (!path.endsWith(FILE_SUFFIX)) { + continue; + } + + File file = new File(path); + String fileName = file.getName(); + if (!fileName.startsWith(FILE_PREFIX)) { + continue; + } + + String locale = fileName.substring(FILE_PREFIX.length(), + fileName.length() - FILE_SUFFIX.length()); + + try { + String content = new String(Files.readAllBytes(file.toPath()), StandardCharsets.UTF_8); + @SuppressWarnings("unchecked") + Map messages = JSONObjectUtil.toObject(content, LinkedHashMap.class); + localeMessages.put(locale, Collections.unmodifiableMap(messages)); + logger.info(String.format("loaded %d i18n error messages for locale [%s]", + messages.size(), locale)); + } catch (Exception e) { + logger.warn(String.format("failed to load i18n file [%s]: %s", path, e.getMessage()), e); + } + } + } catch (Exception e) { + logger.warn(String.format("failed to scan i18n folder: %s", e.getMessage())); + } + + logger.info(String.format("GlobalErrorCodeI18nService loaded %d locales: %s", + localeMessages.size(), localeMessages.keySet())); + } + + @Override + public Set getAvailableLocales() { + return Collections.unmodifiableSet(localeMessages.keySet()); + } + + @Override + public String getLocalizedMessage(String globalErrorCode, String locale, String[] formatArgs) { + if (globalErrorCode == null || locale == null) { + return null; + } + + String template = getTemplate(globalErrorCode, locale); + if (template == null) { + return null; + } + + return formatTemplate(template, formatArgs); + } + + private String getTemplate(String globalErrorCode, String locale) { + Map messages = localeMessages.get(locale); + if (messages != null) { + String template = messages.get(globalErrorCode); + if (template != null) { + return template; + } + } + + // fallback to en_US + if (!"en_US".equals(locale)) { + Map enMessages = localeMessages.get("en_US"); + if (enMessages != null) { + return enMessages.get(globalErrorCode); + } + } + + return null; + } + + private String formatTemplate(String template, String[] formatArgs) { + if (formatArgs == null || formatArgs.length == 0) { + return template; + } + + try { + return String.format(template, (Object[]) formatArgs); + } catch (Exception e) { + logger.debug(String.format("failed to format i18n template [%s]: %s", template, e.getMessage())); + return template; + } + } + + @Override + public void localizeErrorCode(ErrorCode error, String locale) { + if (error == null) { + return; + } + + String resolvedLocale = locale != null ? locale : LocaleUtils.DEFAULT_LOCALE; + + if (error.getGlobalErrorCode() != null) { + 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(), resolvedLocale); + } + + if (error instanceof ErrorCodeList) { + List causes = ((ErrorCodeList) error).getCauses(); + if (causes != null) { + for (ErrorCode cause : causes) { + 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 new file mode 100644 index 00000000000..d97e9c0f0d6 --- /dev/null +++ b/core/src/main/java/org/zstack/core/errorcode/LocaleUtils.java @@ -0,0 +1,93 @@ +package org.zstack.core.errorcode; + +import org.zstack.utils.Utils; +import org.zstack.utils.logging.CLogger; + +import java.util.*; + +public class LocaleUtils { + private static final CLogger logger = Utils.getLogger(LocaleUtils.class); + public static final String DEFAULT_LOCALE = "en_US"; + + private static final Map LANGUAGE_TO_LOCALE = new HashMap<>(); + + // Languages that use underscore format in locale file names (e.g. zh_CN, en_US) + private static final Set UNDERSCORE_LANGS = new HashSet<>(Arrays.asList("zh", "en")); + + static { + LANGUAGE_TO_LOCALE.put("zh", "zh_CN"); + LANGUAGE_TO_LOCALE.put("en", "en_US"); + LANGUAGE_TO_LOCALE.put("ja", "ja-JP"); + LANGUAGE_TO_LOCALE.put("ko", "ko-KR"); + LANGUAGE_TO_LOCALE.put("de", "de-DE"); + LANGUAGE_TO_LOCALE.put("fr", "fr-FR"); + LANGUAGE_TO_LOCALE.put("ru", "ru-RU"); + LANGUAGE_TO_LOCALE.put("th", "th-TH"); + LANGUAGE_TO_LOCALE.put("id", "id-ID"); + } + + /** + * 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 + */ + public static String resolveLocale(String acceptLanguage, Set availableLocales) { + if (acceptLanguage == null || acceptLanguage.trim().isEmpty()) { + return DEFAULT_LOCALE; + } + + 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 tag = range.getRange(); + String normalized = normalizeTag(tag); + if (availableLocales.contains(normalized)) { + return normalized; + } + + String lang = tag.split("[-_]")[0].toLowerCase(); + String mapped = LANGUAGE_TO_LOCALE.get(lang); + if (mapped != null && availableLocales.contains(mapped)) { + return mapped; + } + } + + return DEFAULT_LOCALE; + } + + /** + * Normalize an HTTP language tag to match file locale keys. + * e.g. "zh-CN" -> "zh_CN", "en-US" -> "en_US", "ja-JP" -> "ja-JP" + * See UNDERSCORE_LANGS for languages that use underscore format. + */ + static String normalizeTag(String tag) { + tag = tag.trim(); + String[] parts = tag.split("[-_]"); + if (parts.length == 2) { + String lang = parts[0].toLowerCase(); + String region = parts[1].toUpperCase(); + if (UNDERSCORE_LANGS.contains(lang)) { + return lang + "_" + region; + } + return lang + "-" + region; + } + return tag; + } +} 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/log/LogSafeGson.java b/core/src/main/java/org/zstack/core/log/LogSafeGson.java index c3dcd26626c..13456c13bc0 100644 --- a/core/src/main/java/org/zstack/core/log/LogSafeGson.java +++ b/core/src/main/java/org/zstack/core/log/LogSafeGson.java @@ -208,6 +208,9 @@ private static JsonSerializer getSerializer() { } public static JsonElement toJsonElement(Object o) { + if (o == null) { + return JsonNull.INSTANCE; + } return logSafeGson.toJsonTree(o, getGsonType(o.getClass())); } 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/rest/RESTApiController.java b/core/src/main/java/org/zstack/core/rest/RESTApiController.java index df8657f7f73..3c9e5e642c1 100755 --- a/core/src/main/java/org/zstack/core/rest/RESTApiController.java +++ b/core/src/main/java/org/zstack/core/rest/RESTApiController.java @@ -7,8 +7,13 @@ import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMethod; +import org.zstack.core.errorcode.GlobalErrorCodeI18nService; +import org.zstack.core.errorcode.LocaleUtils; import org.zstack.header.message.APIMessage; import org.zstack.header.message.APISyncCallMessage; +import org.zstack.header.message.APIEvent; +import org.zstack.header.message.Message; +import org.zstack.header.message.MessageReply; import org.zstack.header.rest.RESTApiFacade; import org.zstack.header.rest.RESTConstant; import org.zstack.header.rest.RESTFacade; @@ -30,15 +35,19 @@ public class RESTApiController { private RESTApiFacade restApi; @Autowired private RESTFacade restf; + @Autowired + private GlobalErrorCodeI18nService i18nService; @RequestMapping(value = RESTConstant.REST_API_RESULT + "{uuid}", method = {RequestMethod.GET, RequestMethod.PUT}) - public void queryResult(@PathVariable String uuid, HttpServletResponse rsp) throws IOException { + public void queryResult(@PathVariable String uuid, HttpServletRequest req, HttpServletResponse rsp) throws IOException { try { RestAPIResponse apiRsp = restApi.getResult(uuid); if (apiRsp == null) { rsp.sendError(HttpStatus.SC_NOT_FOUND, String.format("No api result[uuid:%s] found", uuid)); return; } + String locale = resolveLocale(req); + localizeRestAPIResponse(apiRsp, locale); rsp.setCharacterEncoding("UTF-8"); PrintWriter writer = rsp.getWriter(); String res = JSONObjectUtil.toJsonString(apiRsp); @@ -50,7 +59,7 @@ public void queryResult(@PathVariable String uuid, HttpServletResponse rsp) thro } } - private String handleByMessageType(String body, String clientIp, String clientBrowser) { + private String handleByMessageType(String body, String clientIp, String clientBrowser, String locale) { APIMessage amsg = null; try { amsg = (APIMessage) RESTApiDecoder.loads(body); @@ -66,6 +75,7 @@ private String handleByMessageType(String body, String clientIp, String clientBr } else { rsp = restApi.send(amsg); } + localizeRestAPIResponse(rsp, locale); return JSONObjectUtil.toJsonString(rsp); } @@ -74,8 +84,9 @@ public void post(HttpServletRequest request, HttpServletResponse response) throw HttpEntity entity = restf.httpServletRequestToHttpEntity(request); String clientIp = HttpServletRequestUtils.getClientIP(request); String clientBrowser = HttpServletRequestUtils.getClientBrowser(request); + String locale = resolveLocale(request); try { - String ret = handleByMessageType(entity.getBody(), clientIp, clientBrowser); + String ret = handleByMessageType(entity.getBody(), clientIp, clientBrowser, locale); response.setStatus(HttpStatus.SC_OK); response.setCharacterEncoding("UTF-8"); PrintWriter writer = response.getWriter(); @@ -90,4 +101,34 @@ public void post(HttpServletRequest request, HttpServletResponse response) throw } } + private String resolveLocale(HttpServletRequest req) { + String acceptLanguage = req.getHeader("Accept-Language"); + return LocaleUtils.resolveLocale(acceptLanguage, i18nService.getAvailableLocales()); + } + + private void localizeRestAPIResponse(RestAPIResponse rsp, String locale) { + if (rsp == null || rsp.getResult() == null || locale == null) { + return; + } + + try { + Message msg = RESTApiDecoder.loads(rsp.getResult()); + if (msg instanceof MessageReply) { + MessageReply reply = (MessageReply) msg; + if (!reply.isSuccess() && reply.getError() != null) { + i18nService.localizeErrorCode(reply.getError(), locale); + rsp.setResult(RESTApiDecoder.dump(reply)); + } + } else if (msg instanceof APIEvent) { + APIEvent evt = (APIEvent) msg; + if (!evt.isSuccess() && evt.getError() != null) { + i18nService.localizeErrorCode(evt.getError(), locale); + rsp.setResult(RESTApiDecoder.dump(evt)); + } + } + } catch (Exception e) { + logger.debug(String.format("failed to localize RestAPIResponse: %s", e.getMessage()), e); + } + } + } diff --git a/core/src/main/java/org/zstack/core/telemetry/SentryInitHelper.java b/core/src/main/java/org/zstack/core/telemetry/SentryInitHelper.java index 0c04c95801f..66ea7593eb2 100644 --- a/core/src/main/java/org/zstack/core/telemetry/SentryInitHelper.java +++ b/core/src/main/java/org/zstack/core/telemetry/SentryInitHelper.java @@ -1,6 +1,7 @@ package org.zstack.core.telemetry; import io.sentry.Sentry; +import io.sentry.SentryLevel; import org.zstack.core.cloudbus.CloudBusGlobalProperty; import org.zstack.utils.Utils; import org.zstack.utils.logging.CLogger; @@ -54,6 +55,42 @@ public static boolean initIfNeeded() { Sentry.init(options -> { options.setDsn(finalDsn); options.setTracesSampleRate(tracesSampleRate); + options.setMaxQueueSize(1000); + if (TelemetryGlobalProperty.SENTRY_DEBUG) { + options.setDebug(true); + options.setDiagnosticLevel(SentryLevel.DEBUG); + options.setLogger(new io.sentry.ILogger() { + @Override + public void log(SentryLevel level, String message, Object... args) { + String formatted = args.length > 0 ? String.format(message, args) : message; + switch (level) { + case ERROR: logger.warn("[Sentry] " + formatted); break; + case WARNING: logger.warn("[Sentry] " + formatted); break; + default: logger.info("[Sentry] " + formatted); break; + } + } + + @Override + public void log(SentryLevel level, String message, Throwable throwable) { + switch (level) { + case ERROR: logger.warn("[Sentry] " + message, throwable); break; + case WARNING: logger.warn("[Sentry] " + message, throwable); break; + default: logger.info("[Sentry] " + message, throwable); break; + } + } + + @Override + public void log(SentryLevel level, Throwable throwable, String message, Object... args) { + String formatted = args.length > 0 ? String.format(message, args) : message; + log(level, formatted, throwable); + } + + @Override + public boolean isEnabled(SentryLevel level) { + return true; + } + }); + } }); logger.info("Sentry initialized (tracesSampleRate=" + tracesSampleRate + ")"); return true; diff --git a/core/src/main/java/org/zstack/core/telemetry/TelemetryGlobalProperty.java b/core/src/main/java/org/zstack/core/telemetry/TelemetryGlobalProperty.java index 958682ac983..aa1cb3117cd 100644 --- a/core/src/main/java/org/zstack/core/telemetry/TelemetryGlobalProperty.java +++ b/core/src/main/java/org/zstack/core/telemetry/TelemetryGlobalProperty.java @@ -147,6 +147,12 @@ public class TelemetryGlobalProperty { @GlobalProperty(name = "Telemetry.shutdownTimeoutMs", defaultValue = "10000") public static int SHUTDOWN_TIMEOUT_MS; + /** + * Enable Sentry SDK debug logging (outputs to management-server.log via log4j). + */ + @GlobalProperty(name = "Telemetry.sentryDebug", defaultValue = "false") + public static boolean SENTRY_DEBUG; + /** * Environment types enumeration */ 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/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 1b9e9eba7a6..d180b6b4ccb 100755 --- a/header/src/main/java/org/zstack/header/errorcode/ErrorCode.java +++ b/header/src/main/java/org/zstack/header/errorcode/ErrorCode.java @@ -24,6 +24,26 @@ public class ErrorCode implements Serializable, Cloneable { @NoJsonSchema private LinkedHashMap opaque; private String globalErrorCode; + private String message; + @APINoSee + @NoJsonSchema + private String[] formatArgs; + + public String getMessage() { + return message; + } + + public void setMessage(String message) { + this.message = message; + } + + public String[] getFormatArgs() { + return formatArgs == null ? null : formatArgs.clone(); + } + + public void setFormatArgs(String[] formatArgs) { + this.formatArgs = formatArgs == null ? null : formatArgs.clone(); + } public String getGlobalErrorCode() { return globalErrorCode; @@ -81,6 +101,11 @@ public ErrorCode(ErrorCode other) { this.messages = other.messages; this.cause = other.cause; this.location = other.location; + 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/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/storage/addon/primary/ExternalPrimaryStorageInventory.java b/header/src/main/java/org/zstack/header/storage/addon/primary/ExternalPrimaryStorageInventory.java index 7808c227623..a15ed211307 100644 --- a/header/src/main/java/org/zstack/header/storage/addon/primary/ExternalPrimaryStorageInventory.java +++ b/header/src/main/java/org/zstack/header/storage/addon/primary/ExternalPrimaryStorageInventory.java @@ -4,8 +4,10 @@ import org.zstack.header.storage.primary.PrimaryStorageInventory; import org.zstack.utils.gson.JSONObjectUtil; +import java.util.ArrayList; import java.util.LinkedHashMap; import java.util.List; +import java.util.Map; import java.util.stream.Collectors; @Inventory(mappingVOClass = ExternalPrimaryStorageVO.class) @@ -59,6 +61,7 @@ public ExternalPrimaryStorageInventory(ExternalPrimaryStorageVO lvo) { super(lvo); identity = lvo.getIdentity(); config = JSONObjectUtil.toObject(lvo.getConfig(), LinkedHashMap.class); + desensitizeConfig(config); addonInfo = JSONObjectUtil.toObject(lvo.getAddonInfo(), LinkedHashMap.class); outputProtocols = lvo.getOutputProtocols().stream().map(PrimaryStorageOutputProtocolRefVO::getOutputProtocol).collect(Collectors.toList()); defaultProtocol = lvo.getDefaultProtocol(); @@ -68,6 +71,35 @@ public static ExternalPrimaryStorageInventory valueOf(ExternalPrimaryStorageVO l return new ExternalPrimaryStorageInventory(lvo); } + private static void desensitizeConfig(Map config) { + if (config == null) return; + desensitizeUrlList(config, "mdsUrls"); + desensitizeUrlList(config, "mdsInfos"); + } + + private static void desensitizeUrlList(Map config, String key) { + Object urls = config.get(key); + if (urls instanceof List) { + List desensitized = new ArrayList<>(); + for (Object url : (List) urls) { + desensitized.add(desensitizeUrl(String.valueOf(url))); + } + config.put(key, desensitized); + } + } + + private static String desensitizeUrl(String url) { + int atIndex = url.lastIndexOf('@'); + if (atIndex > 0) { + int schemeIndex = url.indexOf("://"); + if (schemeIndex >= 0 && schemeIndex < atIndex) { + return url.substring(0, schemeIndex + 3) + "***" + url.substring(atIndex); + } + return "***" + url.substring(atIndex); + } + return url; + } + public String getIdentity() { return identity; } 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/VmAbnormalLifeCycleStruct.java b/header/src/main/java/org/zstack/header/vm/VmAbnormalLifeCycleStruct.java index 43023821394..7da68329c09 100755 --- a/header/src/main/java/org/zstack/header/vm/VmAbnormalLifeCycleStruct.java +++ b/header/src/main/java/org/zstack/header/vm/VmAbnormalLifeCycleStruct.java @@ -72,7 +72,7 @@ boolean match(VmAbnormalLifeCycleStruct struct) { boolean match(VmAbnormalLifeCycleStruct struct) { return struct.getOriginalState() == VmInstanceState.Paused && struct.getCurrentState() == VmInstanceState.Stopped - && struct.getCurrentHostUuid().equals(struct.getOriginalHostUuid()); + && Objects.equals(struct.getCurrentHostUuid(), struct.getOriginalHostUuid()); } }, VmMigrateToAnotherHost { 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/vm/VmInstanceSpec.java b/header/src/main/java/org/zstack/header/vm/VmInstanceSpec.java index 7007c592aea..99ee2173b98 100755 --- a/header/src/main/java/org/zstack/header/vm/VmInstanceSpec.java +++ b/header/src/main/java/org/zstack/header/vm/VmInstanceSpec.java @@ -847,7 +847,9 @@ public void setBootMode(String bootMode) { public long getRootDiskAllocateSize() { if (rootDiskOffering == null) { - return this.getImageSpec().getInventory().getSize(); + long virtualSize = this.getImageSpec().getInventory().getSize(); + long actualSize = this.getImageSpec().getInventory().getActualSize(); + return Math.max(virtualSize, actualSize); } return rootDiskOffering.getDiskSize(); } diff --git a/header/src/main/java/org/zstack/header/vm/VmInstanceState.java b/header/src/main/java/org/zstack/header/vm/VmInstanceState.java index 8a755b52fda..2f61f0c7e2e 100755 --- a/header/src/main/java/org/zstack/header/vm/VmInstanceState.java +++ b/header/src/main/java/org/zstack/header/vm/VmInstanceState.java @@ -168,8 +168,16 @@ public enum VmInstanceState { new Transaction(VmInstanceStateEvent.destroyed, VmInstanceState.Destroyed), new Transaction(VmInstanceStateEvent.destroying, VmInstanceState.Destroying), new Transaction(VmInstanceStateEvent.running, VmInstanceState.Running), + new Transaction(VmInstanceStateEvent.stopped, VmInstanceState.Stopped), new Transaction(VmInstanceStateEvent.expunging, VmInstanceState.Expunging) ); + // ZSTAC-80898: Expunging safety net — if expunge fails, allow recovery + // instead of leaving VM permanently stuck. + Expunging.transactions( + new Transaction(VmInstanceStateEvent.destroyed, VmInstanceState.Destroyed), + new Transaction(VmInstanceStateEvent.stopped, VmInstanceState.Stopped), + new Transaction(VmInstanceStateEvent.unknown, VmInstanceState.Unknown) + ); Destroyed.transactions( new Transaction(VmInstanceStateEvent.stopped, VmInstanceState.Stopped) ); 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/identity/src/main/java/org/zstack/identity/AuthorizationManager.java b/identity/src/main/java/org/zstack/identity/AuthorizationManager.java index fa50e2b9e8d..daa5152d1f7 100755 --- a/identity/src/main/java/org/zstack/identity/AuthorizationManager.java +++ b/identity/src/main/java/org/zstack/identity/AuthorizationManager.java @@ -116,6 +116,11 @@ public APIMessage intercept(APIMessage msg) throws ApiMessageInterceptionExcepti session = evaluateSession(msg); } + if (session == null) { + throw new ApiMessageInterceptionException(err(ORG_ZSTACK_IDENTITY_10012, IdentityErrors.INVALID_SESSION, + "evaluated session is null for message[%s]", msg.getMessageName())); + } + logger.trace(String.format("authorizing message[%s] with user[accountUuid:%s, uuid:%s] session", msg.getMessageName(), session.getAccountUuid(), 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 5536a5fc487..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()); @@ -1075,6 +1122,13 @@ private void handle(APIGetFreeIpMsg msg) { } limit -= freeIpInventorys.size(); } + + Set reservedIpRanges = self.getReservedIpRanges(); + if (reservedIpRanges != null && !reservedIpRanges.isEmpty()) { + freeIpInventorys.removeIf(freeIp -> reservedIpRanges.stream().anyMatch( + r -> NetworkUtils.isInRange(freeIp.getIp(), r.getStartIp(), r.getEndIp()))); + } + reply.setInventories(freeIpInventorys); bus.reply(msg, reply); 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/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/cbd/src/main/java/org/zstack/cbd/kvm/KvmCbdNodeServer.java b/plugin/cbd/src/main/java/org/zstack/cbd/kvm/KvmCbdNodeServer.java index fe0f69a848a..78439bb113e 100644 --- a/plugin/cbd/src/main/java/org/zstack/cbd/kvm/KvmCbdNodeServer.java +++ b/plugin/cbd/src/main/java/org/zstack/cbd/kvm/KvmCbdNodeServer.java @@ -234,12 +234,12 @@ public void run(MessageReply reply) { private String convertPathIfNeeded(BaseVolumeInfo volumeInfo, HostInventory host){ if (!VolumeProtocol.CBD.name().equals(volumeInfo.getProtocol())){ - return volumeInfo.getInstallPath(); + return null; } PrimaryStorageNodeSvc nodeSvc = getNodeService(volumeInfo); if (nodeSvc == null) { - return volumeInfo.getInstallPath(); + return null; } return nodeSvc.getActivePath(volumeInfo, host, false); @@ -247,7 +247,9 @@ private String convertPathIfNeeded(BaseVolumeInfo volumeInfo, HostInventory host private void convertAndSetPathIfNeeded(BaseVolumeInfo volumeInfo, HostInventory host, T target, PathSetter setter) { String newInstallPath = convertPathIfNeeded(volumeInfo, host); - setter.setPath(target, newInstallPath); + if (newInstallPath != null) { + setter.setPath(target, newInstallPath); + } } 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 d80b40a1d6a..140e94c13a8 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 @@ -3974,15 +3974,17 @@ public void done() { mon.connect(new Completion(releaseLock) { @Override public void success() { + String monUuid = mon.getSelf() == null ? "unknown" : mon.getSelf().getUuid(); logger.debug(String.format("successfully reconnected the mon[uuid:%s] of the ceph primary" + - " storage[uuid:%s, name:%s]", mon.getSelf().getUuid(), self.getUuid(), self.getName())); + " storage[uuid:%s, name:%s]", monUuid, self.getUuid(), self.getName())); releaseLock.done(); } @Override public void fail(ErrorCode errorCode) { + String monUuid = mon.getSelf() == null ? "unknown" : mon.getSelf().getUuid(); logger.warn(String.format("failed to reconnect the mon[uuid:%s] server of the ceph primary" + - " storage[uuid:%s, name:%s], %s", mon.getSelf().getUuid(), self.getUuid(), self.getName(), errorCode)); + " storage[uuid:%s, name:%s], %s", monUuid, self.getUuid(), self.getName(), errorCode)); releaseLock.done(); } }); @@ -5446,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.releaseAvailableCapacity(msg.getSnapshot().getPrimaryStorageInstallPath(), msg.getSnapshot().getSize()); + osdHelper.releaseAvailableCapWithRatio(msg.getSnapshot().getPrimaryStorageInstallPath(), msg.getSnapshot().getSize()); bus.reply(msg, reply); completion.done(); } diff --git a/plugin/ceph/src/main/java/org/zstack/storage/ceph/primary/CephPrimaryStorageMonBase.java b/plugin/ceph/src/main/java/org/zstack/storage/ceph/primary/CephPrimaryStorageMonBase.java index 90b6220d53a..7ca3e710291 100755 --- a/plugin/ceph/src/main/java/org/zstack/storage/ceph/primary/CephPrimaryStorageMonBase.java +++ b/plugin/ceph/src/main/java/org/zstack/storage/ceph/primary/CephPrimaryStorageMonBase.java @@ -141,7 +141,7 @@ public void fail(ErrorCode errorCode) { @Override public String getName() { - return String.format("connect-ceph-primary-storage-mon-%s", self.getUuid()); + return String.format("connect-ceph-primary-storage-mon-%s", self == null ? "unknown" : self.getUuid()); } }); } @@ -420,7 +420,7 @@ public void fail(ErrorCode errorCode) { @Override public String getName() { - return String.format("ping-ceph-primary-storage-%s", self.getUuid()); + return String.format("ping-ceph-primary-storage-%s", self == null ? "unknown" : self.getUuid()); } }); } 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/ExponStorageController.java b/plugin/expon/src/main/java/org/zstack/expon/ExponStorageController.java index 43b2aad3e3f..df5b1081d08 100644 --- a/plugin/expon/src/main/java/org/zstack/expon/ExponStorageController.java +++ b/plugin/expon/src/main/java/org/zstack/expon/ExponStorageController.java @@ -122,6 +122,11 @@ public ExponStorageController(String url) { ExponConnectConfig clientConfig = new ExponConnectConfig(); clientConfig.hostname = uri.getHost(); clientConfig.port = uri.getPort(); + String scheme = uri.getScheme(); + clientConfig.scheme = scheme != null ? scheme : "https"; + if (clientConfig.port == -1) { + clientConfig.port = "https".equalsIgnoreCase(clientConfig.scheme) ? 443 : 80; + } clientConfig.readTimeout = TimeUnit.MINUTES.toMillis(10); clientConfig.writeTimeout = TimeUnit.MINUTES.toMillis(10); ExponClient client = new ExponClient(); diff --git a/plugin/expon/src/main/java/org/zstack/expon/sdk/ExponClient.java b/plugin/expon/src/main/java/org/zstack/expon/sdk/ExponClient.java index 53d087cceac..5ee79354252 100644 --- a/plugin/expon/src/main/java/org/zstack/expon/sdk/ExponClient.java +++ b/plugin/expon/src/main/java/org/zstack/expon/sdk/ExponClient.java @@ -211,7 +211,7 @@ private ApiResult pollResult(String taskId) { private void fillQueryApiRequestBuilder(Request.Builder reqBuilder) throws Exception { ExponQueryRequest qaction = (ExponQueryRequest) action; - HttpUrl.Builder urlBuilder = new HttpUrl.Builder().scheme("https") + HttpUrl.Builder urlBuilder = new HttpUrl.Builder().scheme(config.scheme) .host(config.hostname) .port(config.port); @@ -262,7 +262,7 @@ private void fillQueryApiRequestBuilder(Request.Builder reqBuilder) throws Excep private void fillNonQueryApiRequestBuilder(Request.Builder reqBuilder) throws Exception { HttpUrl.Builder builder = new HttpUrl.Builder() - .scheme("https") + .scheme(config.scheme) .host(config.hostname) .port(config.port); builder.addPathSegment("api"); diff --git a/plugin/expon/src/main/java/org/zstack/expon/sdk/ExponConnectConfig.java b/plugin/expon/src/main/java/org/zstack/expon/sdk/ExponConnectConfig.java index 91725847302..912c247d061 100644 --- a/plugin/expon/src/main/java/org/zstack/expon/sdk/ExponConnectConfig.java +++ b/plugin/expon/src/main/java/org/zstack/expon/sdk/ExponConnectConfig.java @@ -5,6 +5,7 @@ public class ExponConnectConfig { public String hostname = "localhost"; public int port = 443; + public String scheme = "https"; long defaultPollingTimeout = TimeUnit.HOURS.toMillis(3); long defaultPollingInterval = TimeUnit.SECONDS.toMillis(1); public Long readTimeout; @@ -39,6 +40,11 @@ public Builder setPort(int port) { return this; } + public Builder setScheme(String scheme) { + config.scheme = scheme; + return this; + } + public Builder setDefaultPollingTimeout(long value, TimeUnit unit) { config.defaultPollingTimeout = unit.toMillis(value); return this; 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..ecc0c14e2e3 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 @@ -1991,7 +1991,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 { 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..2a36bb5aba3 100755 --- a/plugin/kvm/src/main/java/org/zstack/kvm/KVMAgentCommands.java +++ b/plugin/kvm/src/main/java/org/zstack/kvm/KVMAgentCommands.java @@ -3805,6 +3805,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/KVMGlobalConfig.java b/plugin/kvm/src/main/java/org/zstack/kvm/KVMGlobalConfig.java index f4691e6fb0c..ee765aad40f 100755 --- a/plugin/kvm/src/main/java/org/zstack/kvm/KVMGlobalConfig.java +++ b/plugin/kvm/src/main/java/org/zstack/kvm/KVMGlobalConfig.java @@ -120,6 +120,10 @@ public class KVMGlobalConfig { @GlobalConfigDef(defaultValue = "false", type = Boolean.class, description = "enable install host shutdown hook") public static GlobalConfig INSTALL_HOST_SHUTDOWN_HOOK = new GlobalConfig(CATEGORY, "install.host.shutdown.hook"); + @GlobalConfigValidation(numberGreaterThan = 0) + @GlobalConfigDef(defaultValue = "600", type = Long.class, description = "timeout in seconds for orphaned VM skip entries from departed management nodes") + public static GlobalConfig ORPHANED_VM_SKIP_TIMEOUT = new GlobalConfig(CATEGORY, "vm.orphanedSkipTimeout"); + @GlobalConfigValidation(validValues = {"true", "false"}) @GlobalConfigDef(defaultValue = "false", type = Boolean.class, description = "enable memory auto balloon") @BindResourceConfig({VmInstanceVO.class, HostVO.class, ClusterVO.class}) @@ -135,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..b8b38d1b802 100755 --- a/plugin/kvm/src/main/java/org/zstack/kvm/KVMHost.java +++ b/plugin/kvm/src/main/java/org/zstack/kvm/KVMHost.java @@ -3163,6 +3163,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 +3175,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<>(); @@ -5570,10 +5573,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 +5625,8 @@ public void run(FlowTrigger trigger, Map data) { logger.warn(e.getMessage(), e); trigger.next(); return; + } finally { + ssh.close(); } } }); @@ -5815,6 +5820,16 @@ 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: management IP + extra IPs (migration network etc.) + 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); + } + if (deployArguments.isForceRun()) { runner.setForceRun(true); } 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/KvmVmSyncPingTask.java b/plugin/kvm/src/main/java/org/zstack/kvm/KvmVmSyncPingTask.java index b02525b99d2..f618ab49922 100755 --- a/plugin/kvm/src/main/java/org/zstack/kvm/KvmVmSyncPingTask.java +++ b/plugin/kvm/src/main/java/org/zstack/kvm/KvmVmSyncPingTask.java @@ -69,6 +69,15 @@ public class KvmVmSyncPingTask extends VmTracer implements KVMPingAgentNoFailure private List skipVmTracerReplies = new ArrayList<>(); private Map vmInShutdownMap = new ConcurrentHashMap<>(); + // Orphaned skip entries from departed MN nodes. Key=vmUuid, Value=timestamp when orphaned. + // These VMs remain in skip-trace state to avoid false HA triggers + // when a MN restarts and its in-flight VM operations haven't completed yet. See ZSTAC-80821. + private final ConcurrentHashMap orphanedSkipVms = new ConcurrentHashMap<>(); + + private long getOrphanTtlMs() { + return KVMGlobalConfig.ORPHANED_VM_SKIP_TIMEOUT.value(Long.class) * 1000; + } + { getReflections().getTypesAnnotatedWith(SkipVmTracer.class).forEach(clz -> { skipVmTracerMessages.add(clz.asSubclass(Message.class)); @@ -196,8 +205,13 @@ private void syncVm(final HostInventory host, final Completion completion) { // Get vms to skip before send command to host to confirm the vm will be skipped after sync command finished. // The problem is if one vm-sync skipped operation is started and finished during vm sync command's handling // vm state would still be sync to mn + // ZSTAC-80821: clean up expired orphaned entries each sync cycle + cleanupExpiredOrphanedSkipVms(); + Set vmsToSkipSetHostSide = new HashSet<>(); vmsToSkip.values().forEach(vmsToSkipSetHostSide::addAll); + // ZSTAC-80821: also skip VMs from departed MN nodes that are still within TTL + vmsToSkipSetHostSide.addAll(orphanedSkipVms.keySet()); // if the vm is not running on host when sync command executing but started as soon as possible // before response handling of vm sync, mgmtSideStates will including the running vm but not result in @@ -228,6 +242,8 @@ public void run(MessageReply reply) { // Get vms to skip after sync result returned. vmsToSkip.values().forEach(vmsToSkipSetHostSide::addAll); + // ZSTAC-80821: include orphaned entries from departed MN nodes + vmsToSkipSetHostSide.addAll(orphanedSkipVms.keySet()); Collection vmUuidsInDeleteVmGC = DeleteVmGC.queryVmInGC(host.getUuid(), ret.getStates().keySet()); @@ -446,7 +462,19 @@ public void nodeJoin(ManagementNodeInventory inv) { @Override public void nodeLeft(ManagementNodeInventory inv) { vmApis.remove(inv.getUuid()); - vmsToSkip.remove(inv.getUuid()); + + // ZSTAC-80821: Instead of immediately removing skip list entries, move them + // to the orphaned set with a TTL. This prevents false HA triggers for VMs that + // are still being started by kvmagent but whose controlling MN has restarted. + Set skippedVms = vmsToSkip.remove(inv.getUuid()); + if (skippedVms != null && !skippedVms.isEmpty()) { + long now = System.currentTimeMillis(); + for (String vmUuid : skippedVms) { + orphanedSkipVms.put(vmUuid, now); + logger.info(String.format("moved VM[uuid:%s] from departed MN[uuid:%s] skip list to orphaned set" + + " (will expire in %d minutes)", vmUuid, inv.getUuid(), getOrphanTtlMs() / 60000)); + } + } } @Override @@ -460,6 +488,39 @@ public void iJoin(ManagementNodeInventory inv) { } public boolean isVmDoNotNeedToTrace(String vmUuid) { - return vmsToSkip.values().stream().anyMatch(vmsToSkipSet -> vmsToSkipSet.contains(vmUuid)); + if (vmsToSkip.values().stream().anyMatch(vmsToSkipSet -> vmsToSkipSet.contains(vmUuid))) { + return true; + } + + // ZSTAC-80821: Also check orphaned skip entries from departed MN nodes + Long orphanedAt = orphanedSkipVms.get(vmUuid); + if (orphanedAt != null) { + if (System.currentTimeMillis() - orphanedAt < getOrphanTtlMs()) { + logger.debug(String.format("VM[uuid:%s] is in orphaned skip set, skipping trace", vmUuid)); + return true; + } else { + // Expired, clean up + orphanedSkipVms.remove(vmUuid, orphanedAt); + logger.info(String.format("orphaned skip entry for VM[uuid:%s] expired after %d minutes, resuming trace", + vmUuid, getOrphanTtlMs() / 60000)); + } + } + + return false; + } + + // Periodically clean up expired orphaned entries. Called from VM sync cycle. + private void cleanupExpiredOrphanedSkipVms() { + if (orphanedSkipVms.isEmpty()) { + return; + } + + long now = System.currentTimeMillis(); + for (Map.Entry entry : orphanedSkipVms.entrySet()) { + if (now - entry.getValue() >= getOrphanTtlMs()) { + orphanedSkipVms.remove(entry.getKey(), entry.getValue()); + logger.info(String.format("cleaned up expired orphaned skip entry for VM[uuid:%s]", entry.getKey())); + } + } } } diff --git a/plugin/ldap/src/main/java/org/zstack/ldap/LdapUtil.java b/plugin/ldap/src/main/java/org/zstack/ldap/LdapUtil.java index d386ca2d429..5f4b00f59fb 100644 --- a/plugin/ldap/src/main/java/org/zstack/ldap/LdapUtil.java +++ b/plugin/ldap/src/main/java/org/zstack/ldap/LdapUtil.java @@ -657,10 +657,10 @@ public LdapServerVO getLdapServer() { sq.add(LdapServerVO_.scope, SimpleQuery.Op.EQ, scope); List ldapServers = sq.list(); if (ldapServers.isEmpty()) { - throw new CloudRuntimeException("No LDAP/AD server record in database."); + throw new OperationFailureException(operr(ORG_ZSTACK_LDAP_10020, "No LDAP/AD server record in database.")); } if (ldapServers.size() > 1) { - throw new CloudRuntimeException("More than one LDAP/AD server record in database."); + throw new OperationFailureException(operr(ORG_ZSTACK_LDAP_10021, "More than one LDAP/AD server record in database.")); } return ldapServers.get(0); } 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 97b88c919c2..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,12 +33,14 @@ 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; import org.zstack.network.service.vip.VipVO_; import org.zstack.tag.PatternedSystemTag; import org.zstack.tag.TagManager; +import org.zstack.core.upgrade.UpgradeGlobalConfig; import org.zstack.utils.*; import org.zstack.utils.function.ForEachFunction; import org.zstack.utils.logging.CLogger; @@ -152,10 +154,22 @@ public APIMessage intercept(APIMessage msg) throws ApiMessageInterceptionExcepti validate((APIGetCandidateVmNicsForLoadBalancerServerGroupMsg)msg); } else if (msg instanceof APIChangeLoadBalancerBackendServerMsg) { validate((APIChangeLoadBalancerBackendServerMsg)msg); + } else if (msg instanceof APIDeleteLoadBalancerMsg) { + validate((APIDeleteLoadBalancerMsg) msg); } return msg; } + private void validate(APIDeleteLoadBalancerMsg msg) { + if (UpgradeGlobalConfig.GRAYSCALE_UPGRADE.value(Boolean.class)) { + LoadBalancerVO lb = dbf.findByUuid(msg.getUuid(), LoadBalancerVO.class); + if (lb != null && lb.getType() == LoadBalancerType.SLB) { + throw new ApiMessageInterceptionException(argerr( + "cannot delete the standalone load balancer[uuid:%s] during grayscale upgrade", msg.getUuid())); + } + } + } + private void validate(APIDeleteAccessControlListMsg msg) { /*List refs = Q.New(LoadBalancerListenerACLRefVO.class).select(LoadBalancerListenerACLRefVO_.listenerUuid) .eq(LoadBalancerListenerACLRefVO_.aclUuid, msg.getUuid()).isNull(LoadBalancerListenerACLRefVO_.serverGroupUuid).listValues(); @@ -642,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) { @@ -852,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))) { @@ -1565,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/localstorage/src/main/java/org/zstack/storage/primary/local/LocalStorageAllocatorFactory.java b/plugin/localstorage/src/main/java/org/zstack/storage/primary/local/LocalStorageAllocatorFactory.java index b72673df10a..785b4092144 100755 --- a/plugin/localstorage/src/main/java/org/zstack/storage/primary/local/LocalStorageAllocatorFactory.java +++ b/plugin/localstorage/src/main/java/org/zstack/storage/primary/local/LocalStorageAllocatorFactory.java @@ -385,7 +385,12 @@ private String getHostUuidFromAllocateMsg(AllocatePrimaryStorageSpaceMsg msg) { throw new OperationFailureException( argerr(ORG_ZSTACK_STORAGE_PRIMARY_LOCAL_10023, "invalid uri, correct example is file://$URL;hostUuid://$HOSTUUID or volume://$VOLUMEUUID ")); } - hostUuid = uriParsers.get(protocol).parseUri(msg.getRequiredInstallUri()).hostUuid; + AbstractUriParser parser = uriParsers.get(protocol); + if (parser == null) { + throw new OperationFailureException( + argerr(ORG_ZSTACK_STORAGE_PRIMARY_LOCAL_10023, "unsupported protocol[%s] in uri[%s]", protocol, msg.getRequiredInstallUri())); + } + hostUuid = parser.parseUri(msg.getRequiredInstallUri()).hostUuid; } if (hostUuid != null) { diff --git a/plugin/localstorage/src/main/java/org/zstack/storage/primary/local/LocalStorageKvmBackend.java b/plugin/localstorage/src/main/java/org/zstack/storage/primary/local/LocalStorageKvmBackend.java index 3b78ffab51b..ec0b51711ba 100755 --- a/plugin/localstorage/src/main/java/org/zstack/storage/primary/local/LocalStorageKvmBackend.java +++ b/plugin/localstorage/src/main/java/org/zstack/storage/primary/local/LocalStorageKvmBackend.java @@ -3797,7 +3797,8 @@ public void success(CheckInitializedFileRsp rsp) { @Override public void fail(ErrorCode errorCode) { - completion.fail(operr(ORG_ZSTACK_STORAGE_PRIMARY_LOCAL_10081, "cannot find flag file [%s] on host [%s], because: %s", makeInitializedFilePath(), hostUuid, errorCode.getCause().getDetails())); + String causeDetails = errorCode.getCause() != null ? errorCode.getCause().getDetails() : errorCode.getDetails(); + completion.fail(operr(ORG_ZSTACK_STORAGE_PRIMARY_LOCAL_10081, "cannot find flag file [%s] on host [%s], because: %s", makeInitializedFilePath(), hostUuid, causeDetails)); } }); } @@ -3818,7 +3819,8 @@ public void success(AgentResponse rsp) { @Override public void fail(ErrorCode errorCode) { - completion.fail(operr(ORG_ZSTACK_STORAGE_PRIMARY_LOCAL_10082, "cannot create flag file [%s] on host [%s], because: %s", makeInitializedFilePath(), hostUuid, errorCode.getCause().getDetails())); + String causeDetails = errorCode.getCause() != null ? errorCode.getCause().getDetails() : errorCode.getDetails(); + completion.fail(operr(ORG_ZSTACK_STORAGE_PRIMARY_LOCAL_10082, "cannot create flag file [%s] on host [%s], because: %s", makeInitializedFilePath(), hostUuid, causeDetails)); } }); } 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/zbs/src/main/java/org/zstack/storage/zbs/ZbsStorageController.java b/plugin/zbs/src/main/java/org/zstack/storage/zbs/ZbsStorageController.java index db06239acb3..0d71b507b14 100644 --- a/plugin/zbs/src/main/java/org/zstack/storage/zbs/ZbsStorageController.java +++ b/plugin/zbs/src/main/java/org/zstack/storage/zbs/ZbsStorageController.java @@ -179,7 +179,10 @@ public List getActiveClients(String installPath, String prot if (VolumeProtocol.CBD.toString().equals(protocol)) { GetVolumeClientsCmd cmd = new GetVolumeClientsCmd(); cmd.setPath(installPath); - GetVolumeClientsRsp rsp = syncHttpCall(GET_VOLUME_CLIENTS_PATH, cmd, GetVolumeClientsRsp.class); + GetVolumeClientsRsp rsp = new HttpCaller<>(GET_VOLUME_CLIENTS_PATH, cmd, GetVolumeClientsRsp.class, + null, TimeUnit.SECONDS, 30, true) + .setTryNext(true) + .syncCall(); List clients = new ArrayList<>(); if (!rsp.isSuccess()) { @@ -536,6 +539,7 @@ public void handle(Map data) { error(new FlowErrorHandler(completion) { @Override public void handle(ErrorCode errCode, Map data) { + syncMdsStatuses(newAddonInfo); completion.fail(errCode); } }); @@ -1358,6 +1362,24 @@ public void syncConfig(String config) { this.config = StringUtils.isEmpty(config) ? new Config() : JSONObjectUtil.toObject(config, Config.class); } + private void syncMdsStatuses(AddonInfo newAddonInfo) { + if (addonInfo == null || newAddonInfo == null) { + return; + } + + for (MdsInfo newMds : newAddonInfo.getMdsInfos()) { + for (MdsInfo existMds : addonInfo.getMdsInfos()) { + if (existMds.getAddr().equals(newMds.getAddr())) { + existMds.setStatus(newMds.getStatus()); + } + } + } + + SQL.New(ExternalPrimaryStorageVO.class).eq(ExternalPrimaryStorageVO_.uuid, self.getUuid()) + .set(ExternalPrimaryStorageVO_.addonInfo, JSONObjectUtil.toJsonString(addonInfo)) + .update(); + } + @Deprecated private void reloadDbInfo() { self = dbf.reload(self); @@ -1411,6 +1433,11 @@ public class HttpCaller { private boolean tryNext = false; + HttpCaller setTryNext(boolean tryNext) { + this.tryNext = tryNext; + return this; + } + public HttpCaller(String path, AgentCommand cmd, Class retClass, ReturnValueCompletion callback) { this(path, cmd, retClass, callback, null, 0, false); } diff --git a/pom.xml b/pom.xml index 5173ad0aef2..91f3c4a397a 100755 --- a/pom.xml +++ b/pom.xml @@ -550,17 +550,11 @@ opentelemetry-context 1.35.0 - + io.opentelemetry.semconv opentelemetry-semconv - 1.37.0 - - - - io.opentelemetry - opentelemetry-semconv - 1.23.1-alpha + 1.28.0-alpha diff --git a/portal/src/main/java/org/zstack/portal/managementnode/ManagementNodeManagerImpl.java b/portal/src/main/java/org/zstack/portal/managementnode/ManagementNodeManagerImpl.java index a945ab77274..4ece718ff52 100755 --- a/portal/src/main/java/org/zstack/portal/managementnode/ManagementNodeManagerImpl.java +++ b/portal/src/main/java/org/zstack/portal/managementnode/ManagementNodeManagerImpl.java @@ -74,6 +74,7 @@ import java.sql.SQLException; import java.sql.Timestamp; import java.util.ArrayList; +import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; @@ -107,6 +108,15 @@ public class ManagementNodeManagerImpl extends AbstractService implements Manage // A dictionary (nodeId -> ManagementNodeInventory) of joined management Node final private Map joinedManagementNodes = new ConcurrentHashMap<>(); + // Lock to serialize lifecycle events from heartbeat reconciliation and canonical event callbacks, + // preventing race conditions where a nodeJoin event is immediately followed by a stale nodeLeft + // from the heartbeat thread, or vice versa. See ZSTAC-77711. + private final Object lifecycleLock = new Object(); + + // Track nodes found in hash ring but missing from DB. Only call nodeLeft after a node + // is missing for two consecutive heartbeat cycles, to avoid removing nodes that just joined. + private final Set suspectedMissingFromDb = new HashSet<>(); + private static int NODE_STARTING = 0; private static int NODE_RUNNING = 1; private static int NODE_FAILED = -1; @@ -368,12 +378,16 @@ protected void run(Map tokens, Object data) { ManagementNodeLifeCycleData d = (ManagementNodeLifeCycleData) data; - if (LifeCycle.NodeJoin.toString().equals(d.getLifeCycle())) { - nodeLifeCycle.nodeJoin(d.getInventory()); - } else if (LifeCycle.NodeLeft.toString().equals(d.getLifeCycle())) { - nodeLifeCycle.nodeLeft(d.getInventory()); - } else { - throw new CloudRuntimeException(String.format("unknown lifecycle[%s]", d.getLifeCycle())); + synchronized (lifecycleLock) { + if (LifeCycle.NodeJoin.toString().equals(d.getLifeCycle())) { + // Clear from suspected set since the node is confirmed alive + suspectedMissingFromDb.remove(d.getInventory().getUuid()); + nodeLifeCycle.nodeJoin(d.getInventory()); + } else if (LifeCycle.NodeLeft.toString().equals(d.getLifeCycle())) { + nodeLifeCycle.nodeLeft(d.getInventory()); + } else { + throw new CloudRuntimeException(String.format("unknown lifecycle[%s]", d.getLifeCycle())); + } } } }; @@ -860,34 +874,55 @@ private void checkAllNodesHealth() { Set nodeUuidsInDb = nodesInDb.stream().map(ManagementNodeVO::getUuid).collect(Collectors.toSet()); - // When a node is dying, we may not receive the the dead notification because the message bus may be also dead - // at that moment. By checking if the node UUID is still in our hash ring, we know what nodes should be kicked out - destinationMaker.getManagementNodesInHashRing().forEach(nodeUuid -> { - if (!nodeUuidsInDb.contains(nodeUuid)) { - logger.warn(String.format("found that a management node[uuid:%s] had no heartbeat in database but still in our hash ring," + - "notify that it's dead", nodeUuid)); - ManagementNodeInventory inv = new ManagementNodeInventory(); - inv.setUuid(nodeUuid); - inv.setHostName(destinationMaker.getNodeInfo(nodeUuid).getNodeIP()); - - nodeLifeCycle.nodeLeft(inv); - } - }); - - // check if any node missing in our hash ring - nodesInDb.forEach(n -> { - if (n.getUuid().equals(node().getUuid()) || suspects.contains(n)) { - return; - } - - new Runnable() { - @Override - @AsyncThread - public void run() { - nodeLifeCycle.nodeJoin(ManagementNodeInventory.valueOf(n)); + // Reconcile hash ring with DB under lifecycleLock to prevent race with + // canonical event callbacks (nodeJoin/nodeLeft). See ZSTAC-77711. + synchronized (lifecycleLock) { + // When a node is dying, we may not receive the dead notification because the message bus may be also dead + // at that moment. By checking if the node UUID is still in our hash ring, we know what nodes should be kicked out. + // Use two-round confirmation: first round marks as suspected, second round actually removes. + Set currentSuspected = new HashSet<>(); + destinationMaker.getManagementNodesInHashRing().forEach(nodeUuid -> { + if (!nodeUuidsInDb.contains(nodeUuid)) { + if (suspectedMissingFromDb.contains(nodeUuid)) { + // Second consecutive detection — confirmed missing, remove from hash ring + logger.warn(String.format("management node[uuid:%s] confirmed missing from database for two consecutive" + + " heartbeat cycles, removing from hash ring", nodeUuid)); + ManagementNodeInventory inv = new ManagementNodeInventory(); + inv.setUuid(nodeUuid); + try { + inv.setHostName(destinationMaker.getNodeInfo(nodeUuid).getNodeIP()); + } catch (Exception e) { + logger.warn(String.format("cannot get node info for node[uuid:%s], use empty hostname", nodeUuid)); + } + + nodeLifeCycle.nodeLeft(inv); + } else { + // First detection — mark as suspected, defer removal to next cycle + logger.warn(String.format("management node[uuid:%s] not found in database but still in hash ring," + + " marking as suspected (will remove on next heartbeat if still missing)", nodeUuid)); + currentSuspected.add(nodeUuid); + } } - }.run(); - }); + }); + // Update suspected set: only keep nodes that are newly suspected this round + suspectedMissingFromDb.clear(); + suspectedMissingFromDb.addAll(currentSuspected); + + // check if any node missing in our hash ring + nodesInDb.forEach(n -> { + if (n.getUuid().equals(node().getUuid()) || suspects.contains(n)) { + return; + } + + new Runnable() { + @Override + @AsyncThread + public void run() { + nodeLifeCycle.nodeJoin(ManagementNodeInventory.valueOf(n)); + } + }.run(); + }); + } } @Override diff --git a/rest/src/main/java/org/zstack/rest/RestServer.java b/rest/src/main/java/org/zstack/rest/RestServer.java index 339396d3c41..ee3dbb0b355 100755 --- a/rest/src/main/java/org/zstack/rest/RestServer.java +++ b/rest/src/main/java/org/zstack/rest/RestServer.java @@ -31,6 +31,8 @@ import org.zstack.core.cloudbus.CloudBusEventListener; import org.zstack.core.cloudbus.CloudBusGson; import org.zstack.core.componentloader.PluginRegistry; +import org.zstack.core.errorcode.GlobalErrorCodeI18nService; +import org.zstack.core.errorcode.LocaleUtils; import org.zstack.core.db.Q; import org.zstack.core.log.LogSafeGson; import org.zstack.core.log.LogUtils; @@ -148,6 +150,8 @@ public class RestServer implements Component, CloudBusEventListener { private RESTFacade restf; @Autowired private PluginRegistry pluginRgty; + @Autowired + private GlobalErrorCodeI18nService i18nService; RateLimiter rateLimiter = new RateLimiter(RestGlobalProperty.REST_RATE_LIMITS); @@ -404,6 +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(); + if (!LocaleUtils.DEFAULT_LOCALE.equals(locale)) { + i18nService.localizeErrorCode(evt.getError(), locale); + } response.setError(evt.getError()); } @@ -917,6 +926,12 @@ private void handleJobQuery(HttpServletRequest req, HttpServletResponse rsp) thr } 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); @@ -1411,11 +1426,25 @@ private void writeResponse(ApiResponse response, RestResponseWrapper w, Message } } + private String resolveLocale() { + RequestInfo info = requestInfo.get(); + if (info == null) { + return LocaleUtils.DEFAULT_LOCALE; + } + String acceptLanguage = info.headers.getFirst("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(); if (!reply.isSuccess()) { + 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/rest/src/main/resources/scripts/GoInventory.groovy b/rest/src/main/resources/scripts/GoInventory.groovy index e55be54d3c3..c31e44149c6 100644 --- a/rest/src/main/resources/scripts/GoInventory.groovy +++ b/rest/src/main/resources/scripts/GoInventory.groovy @@ -215,7 +215,13 @@ class GoInventory implements SdkTemplate { // Note: client.go is manually maintained, not auto-generated - // 8. Validate that all referenced response views were generated + // 8. Generate test files (unit tests + integration tests) + def testTemplate = new GoTestTemplate(this, allApiTemplates, inventories) + def testFiles = testTemplate.generate() + files.addAll(testFiles) + logger.warn("[GoSDK] Generated ${testFiles.size()} test files") + + // 9. Validate that all referenced response views were generated validateGeneratedViews() logger.warn("[GoSDK] GoInventory.generate() complete. Total files: " + files.size()) diff --git a/rest/src/main/resources/scripts/GoTestTemplate.groovy b/rest/src/main/resources/scripts/GoTestTemplate.groovy new file mode 100644 index 00000000000..b46b1172adf --- /dev/null +++ b/rest/src/main/resources/scripts/GoTestTemplate.groovy @@ -0,0 +1,547 @@ +package scripts + +import org.zstack.rest.sdk.SdkFile +import org.zstack.utils.Utils +import org.zstack.utils.logging.CLogger + +class GoTestTemplate { + private static final CLogger logger = Utils.getLogger(GoTestTemplate.class) + + private def inventoryGenerator // GoInventory instance + private def allApiTemplates + private def inventories + private Set generatedIntegrationFiles = new HashSet<>() + + GoTestTemplate(def inventoryGenerator, def allApiTemplates, def inventories) { + this.inventoryGenerator = inventoryGenerator + this.allApiTemplates = allApiTemplates + this.inventories = inventories + } + + List generate() { + def files = [] + logger.warn("[GoSDK-Test] Starting test generation...") + + // 1. Static base unit test file + files.add(generateBaseUnitTestFile()) + + // 2. Static base integration test file + files.add(generateBaseIntegrationTestFile()) + + // 2. Per-resource test files + def resourceMap = groupApisByResource() + logger.warn("[GoSDK-Test] Grouped ${resourceMap.size()} resources for test generation") + + resourceMap.each { String prefix, Map resourceInfo -> + String snakeName = toSnakeCase(prefix) + + // Unit tests + def paramTest = generateParamTestFile(prefix, snakeName, resourceInfo) + if (paramTest != null) files.add(paramTest) + + def viewTest = generateViewTestFile(prefix, snakeName, resourceInfo) + if (viewTest != null) files.add(viewTest) + + // TODO: client tests require parsing actual method signatures from GoApiTemplate + // Will be added in a follow-up iteration + // def clientTest = generateClientTestFile(prefix, snakeName, resourceInfo) + // if (clientTest != null) files.add(clientTest) + + // Integration tests (generated to pkg/integration_test/ to avoid conflicts) + def integrationTest = generateIntegrationTestFile(prefix, snakeName, resourceInfo) + if (integrationTest != null) files.add(integrationTest) + } + + logger.warn("[GoSDK-Test] Test generation complete. Generated ${files.size()} test files") + return files + } + + // ======================== Resource Grouping ======================== + + private Map groupApisByResource() { + def resourceMap = [:] + + inventories.each { Class inventoryClass -> + String prefix = inventoryClass.simpleName.replaceAll('Inventory\$', '') + if (!resourceMap.containsKey(prefix)) { + resourceMap[prefix] = [ + inventoryClass: inventoryClass, + viewStructName: inventoryGenerator.getViewStructName(inventoryClass), + templates: [] + ] + } + } + + allApiTemplates.each { template -> + String resName = template.getResourceName() + if (resName != null && resourceMap.containsKey(resName)) { + resourceMap[resName].templates.add(template) + } + } + + // Filter out resources with no templates + return resourceMap.findAll { k, v -> !v.templates.isEmpty() } + } + + // ======================== Base Unit Test ======================== + + private SdkFile generateBaseUnitTestFile() { + def content = new StringBuilder() + content.append('''\ +// Copyright (c) ZStack.io, Inc. +// Auto-generated test infrastructure. DO NOT EDIT. + +package unit_test + +import ( +\t"encoding/json" +\t"fmt" +\t"net/http" +\t"net/http/httptest" +\t"strings" +\t"testing" +\t"time" + +\t"github.com/zstackio/zstack-sdk-go-v2/pkg/client" +) + +// newMockClient creates a ZSClient backed by an httptest server. +// The handler receives all HTTP requests and can assert on method/path/body. +func newMockClient(handler http.HandlerFunc) (*client.ZSClient, func()) { +\tserver := httptest.NewServer(handler) +\t// Parse host and port from server URL +\taddr := server.Listener.Addr().String() +\tparts := strings.SplitN(addr, ":", 2) +\thost := parts[0] +\tport := 80 +\tif len(parts) == 2 { +\t\tfmt.Sscanf(parts[1], "%d", &port) +\t} + +\tconfig := client.NewZSConfig(host, port, "") +\tconfig.LoginAccount("admin", "password") +\tcli := client.NewZSClient(config) +\tcli.LoadSession("mock-session-id") +\treturn cli, server.Close +} + +// mockInventoryResponse builds a JSON response wrapping data in {"inventory": ...} +func mockInventoryResponse(data map[string]interface{}) []byte { +\tresp := map[string]interface{}{"inventory": data} +\tb, _ := json.Marshal(resp) +\treturn b +} + +// mockInventoriesResponse builds a JSON response wrapping data in {"inventories": [...]} +func mockInventoriesResponse(items ...map[string]interface{}) []byte { +\tresp := map[string]interface{}{"inventories": items} +\tb, _ := json.Marshal(resp) +\treturn b +} + +// stringPtr returns a pointer to the given string. +func stringPtr(s string) *string { +\treturn &s +} + +// timePtr parses a time string and returns a pointer. +func timePtr(s string) *time.Time { +\tt, _ := time.Parse(time.RFC3339, s) +\treturn &t +} + +// assertEqual is a simple test helper. +func assertEqual(t *testing.T, expected, actual interface{}) { +\tt.Helper() +\tif expected != actual { +\t\tt.Errorf("expected %v, got %v", expected, actual) +\t} +} + +// assertNoError fails the test if err is not nil. +func assertNoError(t *testing.T, err error) { +\tt.Helper() +\tif err != nil { +\t\tt.Fatalf("unexpected error: %v", err) +\t} +} + +// assertContains checks that s contains substr. +func assertContains(t *testing.T, s, substr string) { +\tt.Helper() +\tif !strings.Contains(s, substr) { +\t\tt.Errorf("expected %q to contain %q", s, substr) +\t} +} +''') + + def sdkFile = new SdkFile() + sdkFile.subPath = "/pkg/unit_test/" + sdkFile.fileName = "base_unit_test.go" + sdkFile.content = content.toString() + return sdkFile + } + + // ======================== Param Tests ======================== + + private SdkFile generateParamTestFile(String prefix, String snakeName, Map resourceInfo) { + def templates = resourceInfo.templates + def paramTemplates = templates.findAll { t -> + !t.isQueryMessage() && t.getActionType() in ['Create', 'Update', 'Add'] + } + if (paramTemplates.isEmpty()) return null + + def content = new StringBuilder() + content.append("// Copyright (c) ZStack.io, Inc.\n") + content.append("// Auto-generated param tests. DO NOT EDIT.\n\n") + content.append("package unit_test\n\n") + content.append("import (\n") + content.append("\t\"encoding/json\"\n") + content.append("\t\"testing\"\n\n") + content.append("\t\"github.com/zstackio/zstack-sdk-go-v2/pkg/param\"\n") + content.append(")\n\n") + + paramTemplates.each { template -> + String paramStruct = template.getParamStructName() + String detailStruct = template.getDetailParamStructName() + String methodName = template.clzName + + // Marshal test - zero value should produce valid JSON + content.append("func Test${methodName}Param_MarshalJSON(t *testing.T) {\n") + content.append("\tp := param.${paramStruct}{}\n") + content.append("\tdata, err := json.Marshal(p)\n") + content.append("\tassertNoError(t, err)\n") + content.append("\tif len(data) == 0 {\n") + content.append("\t\tt.Fatal(\"marshaled JSON should not be empty\")\n") + content.append("\t}\n") + content.append("\t// Verify it's valid JSON\n") + content.append("\tvar raw map[string]interface{}\n") + content.append("\tassertNoError(t, json.Unmarshal(data, &raw))\n") + content.append("}\n\n") + + // Unmarshal test - minimal JSON should parse without error + content.append("func Test${methodName}Param_UnmarshalJSON(t *testing.T) {\n") + content.append("\tjsonStr := `{}`\n") + content.append("\tvar p param.${paramStruct}\n") + content.append("\terr := json.Unmarshal([]byte(jsonStr), &p)\n") + content.append("\tassertNoError(t, err)\n") + content.append("}\n\n") + } + + def sdkFile = new SdkFile() + sdkFile.subPath = "/pkg/unit_test/" + sdkFile.fileName = "${snakeName}_param_test.go" + sdkFile.content = content.toString() + return sdkFile + } + + // ======================== View Tests ======================== + + private SdkFile generateViewTestFile(String prefix, String snakeName, Map resourceInfo) { + String viewStructName = resourceInfo.viewStructName + if (viewStructName == null) return null + + def content = new StringBuilder() + content.append("// Copyright (c) ZStack.io, Inc.\n") + content.append("// Auto-generated view tests. DO NOT EDIT.\n\n") + content.append("package unit_test\n\n") + content.append("import (\n") + content.append("\t\"encoding/json\"\n") + content.append("\t\"testing\"\n\n") + content.append("\t\"github.com/zstackio/zstack-sdk-go-v2/pkg/view\"\n") + content.append(")\n\n") + + // InventoryView unmarshal test + content.append("func Test${viewStructName}_UnmarshalJSON(t *testing.T) {\n") + content.append("\tjsonStr := `{\n") + content.append("\t\t\"uuid\": \"test-uuid-001\",\n") + content.append("\t\t\"name\": \"test-${snakeName}\",\n") + content.append("\t\t\"createDate\": \"2024-01-01T00:00:00.000+08:00\",\n") + content.append("\t\t\"lastOpDate\": \"2024-01-01T00:00:00.000+08:00\"\n") + content.append("\t}`\n") + content.append("\tvar v view.${viewStructName}\n") + content.append("\terr := json.Unmarshal([]byte(jsonStr), &v)\n") + content.append("\tassertNoError(t, err)\n") + content.append("}\n\n") + + // Empty JSON should not error + content.append("func Test${viewStructName}_UnmarshalEmpty(t *testing.T) {\n") + content.append("\tvar v view.${viewStructName}\n") + content.append("\terr := json.Unmarshal([]byte(`{}`), &v)\n") + content.append("\tassertNoError(t, err)\n") + content.append("}\n\n") + + def sdkFile = new SdkFile() + sdkFile.subPath = "/pkg/unit_test/" + sdkFile.fileName = "${snakeName}_view_test.go" + sdkFile.content = content.toString() + return sdkFile + } + + // ======================== Client Tests ======================== + + private SdkFile generateClientTestFile(String prefix, String snakeName, Map resourceInfo) { + def templates = resourceInfo.templates + if (templates.isEmpty()) return null + + String viewStructName = resourceInfo.viewStructName + + def content = new StringBuilder() + content.append("// Copyright (c) ZStack.io, Inc.\n") + content.append("// Auto-generated client tests. DO NOT EDIT.\n\n") + content.append("package unit_test\n\n") + content.append("import (\n") + content.append("\t\"net/http\"\n") + content.append("\t\"testing\"\n\n") + content.append("\t\"github.com/zstackio/zstack-sdk-go-v2/pkg/param\"\n") + content.append(")\n\n") + content.append("var _ = param.BaseParam{} // avoid unused import\n\n") + + templates.each { template -> + String actionType = template.getActionType() + String methodName = template.clzName + + if (template.isQueryMessage()) { + // Query test + content.append(generateQueryClientTest(prefix, methodName, snakeName)) + // Get test (derived from Query) + String getMethodName = methodName.replaceFirst('^Query', 'Get') + content.append(generateGetClientTest(prefix, getMethodName, snakeName)) + } else if (actionType == 'Create' || actionType == 'Add') { + content.append(generateCreateClientTest(prefix, methodName, snakeName, template.getParamStructName())) + } else if (actionType == 'Update' || actionType == 'Change') { + content.append(generateUpdateClientTest(prefix, methodName, snakeName, template.getParamStructName())) + } else if (actionType == 'Delete') { + content.append(generateDeleteClientTest(prefix, methodName, snakeName)) + } + } + + if (content.toString().count("func Test") == 0) return null + + def sdkFile = new SdkFile() + sdkFile.subPath = "/pkg/unit_test/" + sdkFile.fileName = "${snakeName}_client_test.go" + sdkFile.content = content.toString() + return sdkFile + } + + private String generateQueryClientTest(String prefix, String methodName, String snakeName) { + return """\ +func Test${methodName}_Client(t *testing.T) { +\tcli, cleanup := newMockClient(func(w http.ResponseWriter, r *http.Request) { +\t\tassertEqual(t, http.MethodGet, r.Method) +\t\tw.WriteHeader(http.StatusOK) +\t\tw.Write(mockInventoriesResponse(map[string]interface{}{ +\t\t\t"uuid": "test-uuid-001", +\t\t\t"name": "test-${snakeName}", +\t\t})) +\t}) +\tdefer cleanup() + +\tqueryParam := param.NewQueryParam() +\tresult, err := cli.${methodName}(&queryParam) +\tassertNoError(t, err) +\tif len(result) == 0 { +\t\tt.Fatal("expected at least one result") +\t} +\tassertEqual(t, "test-uuid-001", result[0].UUID) +} + +""" + } + + private String generateGetClientTest(String prefix, String methodName, String snakeName) { + return """\ +func Test${methodName}_Client(t *testing.T) { +\tcli, cleanup := newMockClient(func(w http.ResponseWriter, r *http.Request) { +\t\tassertEqual(t, http.MethodGet, r.Method) +\t\tw.WriteHeader(http.StatusOK) +\t\tw.Write(mockInventoriesResponse(map[string]interface{}{ +\t\t\t"uuid": "test-uuid-001", +\t\t\t"name": "test-${snakeName}", +\t\t})) +\t}) +\tdefer cleanup() + +\tresult, err := cli.${methodName}("test-uuid-001") +\tassertNoError(t, err) +\tassertEqual(t, "test-uuid-001", result.UUID) +} + +""" + } + + private String generateCreateClientTest(String prefix, String methodName, String snakeName, String paramStruct) { + return """\ +func Test${methodName}_Client(t *testing.T) { +\tcli, cleanup := newMockClient(func(w http.ResponseWriter, r *http.Request) { +\t\tassertEqual(t, http.MethodPost, r.Method) +\t\tw.WriteHeader(http.StatusOK) +\t\tw.Write(mockInventoryResponse(map[string]interface{}{ +\t\t\t"uuid": "new-uuid-001", +\t\t\t"name": "test-${snakeName}", +\t\t})) +\t}) +\tdefer cleanup() + +\tresult, err := cli.${methodName}(param.${paramStruct}{}) +\tassertNoError(t, err) +\tassertEqual(t, "new-uuid-001", result.UUID) +} + +""" + } + + private String generateUpdateClientTest(String prefix, String methodName, String snakeName, String paramStruct) { + return """\ +func Test${methodName}_Client(t *testing.T) { +\tcli, cleanup := newMockClient(func(w http.ResponseWriter, r *http.Request) { +\t\tassertEqual(t, http.MethodPut, r.Method) +\t\tw.WriteHeader(http.StatusOK) +\t\tw.Write(mockInventoryResponse(map[string]interface{}{ +\t\t\t"uuid": "test-uuid-001", +\t\t\t"name": "updated-${snakeName}", +\t\t})) +\t}) +\tdefer cleanup() + +\tresult, err := cli.${methodName}("test-uuid-001", param.${paramStruct}{}) +\tassertNoError(t, err) +\tassertEqual(t, "test-uuid-001", result.UUID) +} + +""" + } + + private String generateDeleteClientTest(String prefix, String methodName, String snakeName) { + return """\ +func Test${methodName}_Client(t *testing.T) { +\tcli, cleanup := newMockClient(func(w http.ResponseWriter, r *http.Request) { +\t\tassertEqual(t, http.MethodDelete, r.Method) +\t\tw.WriteHeader(http.StatusOK) +\t\tw.Write([]byte(`{}`)) +\t}) +\tdefer cleanup() + +\terr := cli.${methodName}("test-uuid-001", param.DeleteModePermissive) +\tassertNoError(t, err) +} + +""" + } + + // ======================== Integration Tests ======================== + + private SdkFile generateBaseIntegrationTestFile() { + def content = new StringBuilder() + content.append('''\ +// Copyright (c) ZStack.io, Inc. +// Auto-generated integration test infrastructure. DO NOT EDIT. + +package integration_test + +import ( +\t"os" +\t"testing" + +\t"github.com/kataras/golog" +\t"github.com/zstackio/zstack-sdk-go-v2/pkg/client" +) + +const ( +\tdefaultHostname = "localhost" +\tdefaultAccount = "admin" +\tdefaultPassword = "password" +) + +var testCli *client.ZSClient + +func TestMain(m *testing.M) { +\thostname := os.Getenv("ZSTACK_HOST") +\tif hostname == "" { +\t\thostname = defaultHostname +\t} +\taccount := os.Getenv("ZSTACK_ACCOUNT") +\tif account == "" { +\t\taccount = defaultAccount +\t} +\tpassword := os.Getenv("ZSTACK_PASSWORD") +\tif password == "" { +\t\tpassword = defaultPassword +\t} + +\tconfig := client.DefaultZSConfig(hostname). +\t\tLoginAccount(account, password). +\t\tDebug(true) +\ttestCli = client.NewZSClient(config) + +\t_, err := testCli.Login() +\tif err != nil { +\t\tgolog.Errorf("Integration test login failed: %v", err) +\t\tos.Exit(1) +\t} +\tdefer testCli.Logout() + +\tos.Exit(m.Run()) +} +''') + + def sdkFile = new SdkFile() + sdkFile.subPath = "/pkg/integration_test/" + sdkFile.fileName = "base_test.go" + sdkFile.content = content.toString() + return sdkFile + } + + private SdkFile generateIntegrationTestFile(String prefix, String snakeName, Map resourceInfo) { + def templates = resourceInfo.templates + + // Only generate for resources that have a Query API + def queryTemplate = templates.find { it.isQueryMessage() } + if (queryTemplate == null) return null + + String fileName = "${snakeName}_query_test.go" + + // Track generated filenames to avoid collisions within this run + if (generatedIntegrationFiles.contains(fileName)) { + logger.warn("[GoSDK-Test] Skipping duplicate integration test file: ${fileName}") + return null + } + generatedIntegrationFiles.add(fileName) + + String methodName = queryTemplate.clzName // e.g. "QueryCluster" + + def content = new StringBuilder() + content.append("// Copyright (c) ZStack.io, Inc.\n") + content.append("// Auto-generated integration tests. DO NOT EDIT.\n\n") + content.append("package integration_test\n\n") + content.append("import (\n") + content.append("\t\"testing\"\n\n") + content.append("\t\"github.com/kataras/golog\"\n\n") + content.append("\t\"github.com/zstackio/zstack-sdk-go-v2/pkg/param\"\n") + content.append(")\n\n") + + // Query test + content.append("func Test${methodName}(t *testing.T) {\n") + content.append("\tqueryParam := param.NewQueryParam()\n") + content.append("\tresult, err := testCli.${methodName}(&queryParam)\n") + content.append("\tif err != nil {\n") + content.append("\t\tt.Errorf(\"Test${methodName} error: %v\", err)\n") + content.append("\t\treturn\n") + content.append("\t}\n") + content.append("\tgolog.Infof(\"${methodName} result count: %d\", len(result))\n") + content.append("}\n\n") + + def sdkFile = new SdkFile() + sdkFile.subPath = "/pkg/integration_test/" + sdkFile.fileName = fileName + sdkFile.content = content.toString() + return sdkFile + } + + // ======================== Utilities ======================== + + private String toSnakeCase(String name) { + return name.replaceAll('([a-z])([A-Z])', '$1_$2').toLowerCase() + } +} diff --git a/sdk/src/main/java/SourceClassMap.java b/sdk/src/main/java/SourceClassMap.java index 4bbd9238f98..614350ecd69 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"); @@ -65,8 +66,10 @@ 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.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 +247,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"); @@ -870,6 +875,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 +893,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 +960,8 @@ 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.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"); @@ -1062,6 +1074,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"); @@ -1206,6 +1219,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"); @@ -1744,6 +1758,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/AddModelAction.java b/sdk/src/main/java/org/zstack/sdk/AddModelAction.java index 993a073b9c3..6e3d3d27bc2 100644 --- a/sdk/src/main/java/org/zstack/sdk/AddModelAction.java +++ b/sdk/src/main/java/org/zstack/sdk/AddModelAction.java @@ -70,6 +70,9 @@ public Result throwExceptionIfError() { @Param(required = false, maxLength = 255, nonempty = false, nullElements = false, emptyString = true, noTrim = false) public java.lang.String modelId; + @Param(required = false, validValues = {"Public"}, nonempty = false, nullElements = false, emptyString = true, noTrim = false) + public java.lang.String shareMode; + @Param(required = false) public java.lang.String resourceUuid; 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..a68225a7c45 --- /dev/null +++ b/sdk/src/main/java/org/zstack/sdk/BareMetal2DpuChassisInventory.java @@ -0,0 +1,23 @@ +package org.zstack.sdk; + + + +public class BareMetal2DpuChassisInventory extends org.zstack.sdk.BareMetal2ChassisInventory { + + public java.lang.String config; + public void setConfig(java.lang.String config) { + this.config = config; + } + public java.lang.String getConfig() { + return this.config; + } + + public java.lang.String hostUuid; + public void setHostUuid(java.lang.String hostUuid) { + this.hostUuid = hostUuid; + } + public java.lang.String getHostUuid() { + return this.hostUuid; + } + +} 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/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/ErrorCode.java b/sdk/src/main/java/org/zstack/sdk/ErrorCode.java index 68f2b843c67..e8f4c69d46d 100755 --- a/sdk/src/main/java/org/zstack/sdk/ErrorCode.java +++ b/sdk/src/main/java/org/zstack/sdk/ErrorCode.java @@ -76,4 +76,12 @@ public java.lang.String getGlobalErrorCode() { return this.globalErrorCode; } + public java.lang.String message; + public void setMessage(java.lang.String message) { + this.message = message; + } + public java.lang.String getMessage() { + return this.message; + } + } 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/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/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/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/ModelEvaluationTaskInventory.java b/sdk/src/main/java/org/zstack/sdk/ModelEvaluationTaskInventory.java index 7695a4c3ace..3dcf79a660b 100644 --- a/sdk/src/main/java/org/zstack/sdk/ModelEvaluationTaskInventory.java +++ b/sdk/src/main/java/org/zstack/sdk/ModelEvaluationTaskInventory.java @@ -228,4 +228,20 @@ public java.lang.Integer getReadTimeout() { return this.readTimeout; } + public java.lang.Double totalScore; + public void setTotalScore(java.lang.Double totalScore) { + this.totalScore = totalScore; + } + public java.lang.Double getTotalScore() { + return this.totalScore; + } + + public java.sql.Timestamp endTime; + public void setEndTime(java.sql.Timestamp endTime) { + this.endTime = endTime; + } + public java.sql.Timestamp getEndTime() { + return this.endTime; + } + } diff --git a/sdk/src/main/java/org/zstack/sdk/ModelServiceInstanceGroupInventory.java b/sdk/src/main/java/org/zstack/sdk/ModelServiceInstanceGroupInventory.java index 25ad17e970d..2451290084a 100644 --- a/sdk/src/main/java/org/zstack/sdk/ModelServiceInstanceGroupInventory.java +++ b/sdk/src/main/java/org/zstack/sdk/ModelServiceInstanceGroupInventory.java @@ -116,4 +116,12 @@ public java.util.List getSupportMetrics() { return this.supportMetrics; } + public java.lang.String exportPath; + public void setExportPath(java.lang.String exportPath) { + this.exportPath = exportPath; + } + public java.lang.String getExportPath() { + return this.exportPath; + } + } 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/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/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/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/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/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/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/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/search/src/main/java/org/zstack/query/QueryFacadeImpl.java b/search/src/main/java/org/zstack/query/QueryFacadeImpl.java index 6dafe940fca..9bd8fa8c724 100755 --- a/search/src/main/java/org/zstack/query/QueryFacadeImpl.java +++ b/search/src/main/java/org/zstack/query/QueryFacadeImpl.java @@ -378,7 +378,7 @@ private void handle(APIQueryMessage msg) { } catch (OperationFailureException of) { throw of; } catch (Exception e) { - throw new CloudRuntimeException(e); + throw new OperationFailureException(inerr("failed to query: %s", e.getMessage())); } } diff --git a/storage/src/main/java/org/zstack/storage/addon/primary/ExternalPrimaryStorage.java b/storage/src/main/java/org/zstack/storage/addon/primary/ExternalPrimaryStorage.java index 673cfed7c12..332b10331a1 100644 --- a/storage/src/main/java/org/zstack/storage/addon/primary/ExternalPrimaryStorage.java +++ b/storage/src/main/java/org/zstack/storage/addon/primary/ExternalPrimaryStorage.java @@ -510,8 +510,19 @@ private void handle(final SelectBackupStorageMsg msg) { .param("size", msg.getRequiredSize()) .list(); - // sort by prefer type - availableBs.sort(Comparator.comparingInt(o -> preferBsTypes.indexOf(o.getType()))); + // filter out non-preferred types, then sort by preference order + availableBs = availableBs.stream() + .filter(bs -> preferBsTypes.contains(bs.getType())) + .sorted(Comparator.comparingInt(o -> preferBsTypes.indexOf(o.getType()))) + .collect(Collectors.toList()); + + if (availableBs.isEmpty()) { + reply.setError(operr(ORG_ZSTACK_STORAGE_ADDON_PRIMARY_10015, + "no available backup storage with preferred types %s for primary storage[uuid:%s]", + preferBsTypes, self.getUuid())); + bus.reply(msg, reply); + return; + } reply.setInventory(BackupStorageInventory.valueOf(availableBs.get(0))); bus.reply(msg, reply); diff --git a/storage/src/main/java/org/zstack/storage/addon/primary/ExternalPrimaryStorageFactory.java b/storage/src/main/java/org/zstack/storage/addon/primary/ExternalPrimaryStorageFactory.java index 57456c18d98..ffabd743037 100644 --- a/storage/src/main/java/org/zstack/storage/addon/primary/ExternalPrimaryStorageFactory.java +++ b/storage/src/main/java/org/zstack/storage/addon/primary/ExternalPrimaryStorageFactory.java @@ -426,6 +426,12 @@ public void preReleaseVmResource(VmInstanceSpec spec, Completion completion) { return; } + if (spec.getDestHost() == null) { + logger.debug("skip deactivate volumes because no host associated"); + completion.success(); + return; + } + deactivateVolumes(vols, spec.getDestHost(), completion); } 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/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/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..d4a30cef78e --- /dev/null +++ b/test/src/test/groovy/org/zstack/test/integration/kvm/vm/migrate/LibvirtTlsMigrateCase.groovy @@ -0,0 +1,267 @@ +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.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. + * + * 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 { + 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" + } + } +} 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/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/integration/rest/ErrorCodeI18nCase.groovy b/test/src/test/groovy/org/zstack/test/integration/rest/ErrorCodeI18nCase.groovy new file mode 100644 index 00000000000..8c7e793e544 --- /dev/null +++ b/test/src/test/groovy/org/zstack/test/integration/rest/ErrorCodeI18nCase.groovy @@ -0,0 +1,316 @@ +package org.zstack.test.integration.rest + +import org.zstack.core.errorcode.GlobalErrorCodeI18nService +import org.zstack.header.errorcode.ErrorCode +import org.zstack.header.errorcode.ErrorCodeList +import org.zstack.header.errorcode.SysErrors +import org.zstack.header.zone.APIQueryZoneMsg +import org.zstack.test.integration.ZStackTest +import org.zstack.testlib.EnvSpec +import org.zstack.testlib.SubCase +import org.zstack.testlib.WebBeanConstructor +import org.zstack.utils.gson.JSONObjectUtil + +class ErrorCodeI18nCase extends SubCase { + EnvSpec env + + @Override + void clean() { + env.delete() + } + + @Override + void setup() { + useSpring(ZStackTest.springSpec) + } + + @Override + void environment() { + env = env {} + } + + @Override + void test() { + env.create { + testServiceLocalizesZhCN() + testServiceLocalizesEnUS() + testServiceFallbackToEnUS() + testServiceFormatsArgs() + testServiceRecursiveCauseChain() + testServiceErrorCodeList() + testRestServerSyncApiWithAcceptLanguage() + testRestServerAsyncApiWithAcceptLanguage() + testNoAcceptLanguageHeaderFallsBackToEnUS() + } + } + + void testServiceLocalizesZhCN() { + GlobalErrorCodeI18nService i18nService = bean(GlobalErrorCodeI18nService.class) + // ORG_ZSTACK_STORAGE_PRIMARY_10039: zh_CN = "未找到主存储[uuid:%s]" + String msg = i18nService.getLocalizedMessage( + "ORG_ZSTACK_STORAGE_PRIMARY_10039", "zh_CN", ["test-uuid-123"] as String[]) + assert msg != null + assert msg.contains("未找到主存储") + assert msg.contains("test-uuid-123") + } + + void testServiceLocalizesEnUS() { + GlobalErrorCodeI18nService i18nService = bean(GlobalErrorCodeI18nService.class) + // ORG_ZSTACK_STORAGE_PRIMARY_10039: en_US = "no primary storage[uuid:%s] exists" + String msg = i18nService.getLocalizedMessage( + "ORG_ZSTACK_STORAGE_PRIMARY_10039", "en_US", ["test-uuid-456"] as String[]) + assert msg != null + assert msg.contains("no primary storage") + assert msg.contains("test-uuid-456") + } + + void testServiceFallbackToEnUS() { + GlobalErrorCodeI18nService i18nService = bean(GlobalErrorCodeI18nService.class) + // Request a locale that doesn't exist, should fallback to en_US + String msg = i18nService.getLocalizedMessage( + "ORG_ZSTACK_STORAGE_PRIMARY_10039", "nonexistent_locale", ["uuid-789"] as String[]) + assert msg != null + assert msg.contains("no primary storage") + } + + void testServiceFormatsArgs() { + GlobalErrorCodeI18nService i18nService = bean(GlobalErrorCodeI18nService.class) + // Test with null formatArgs - should return template as-is + String msg = i18nService.getLocalizedMessage( + "ORG_ZSTACK_STORAGE_PRIMARY_10039", "zh_CN", null) + assert msg != null + assert msg.contains("%s") // template not formatted + } + + void testServiceRecursiveCauseChain() { + GlobalErrorCodeI18nService i18nService = bean(GlobalErrorCodeI18nService.class) + + ErrorCode cause = new ErrorCode() + cause.setCode(SysErrors.INTERNAL.toString()) + cause.setGlobalErrorCode("ORG_ZSTACK_STORAGE_PRIMARY_10039") + cause.setFormatArgs(["inner-uuid"] as String[]) + + ErrorCode outer = new ErrorCode() + outer.setCode(SysErrors.INTERNAL.toString()) + outer.setGlobalErrorCode("ORG_ZSTACK_STORAGE_PRIMARY_10039") + outer.setFormatArgs(["outer-uuid"] as String[]) + outer.setCause(cause) + + i18nService.localizeErrorCode(outer, "zh_CN") + + assert outer.getMessage() != null + assert outer.getMessage().contains("outer-uuid") + assert outer.getCause().getMessage() != null + assert outer.getCause().getMessage().contains("inner-uuid") + } + + void testServiceErrorCodeList() { + GlobalErrorCodeI18nService i18nService = bean(GlobalErrorCodeI18nService.class) + + ErrorCodeList errorList = new ErrorCodeList() + errorList.setCode(SysErrors.INTERNAL.toString()) + errorList.setGlobalErrorCode("ORG_ZSTACK_STORAGE_PRIMARY_10039") + errorList.setFormatArgs(["list-uuid"] as String[]) + + ErrorCode child1 = new ErrorCode() + child1.setCode(SysErrors.INTERNAL.toString()) + child1.setGlobalErrorCode("ORG_ZSTACK_STORAGE_PRIMARY_10039") + child1.setFormatArgs(["child1-uuid"] as String[]) + + ErrorCode child2 = new ErrorCode() + child2.setCode(SysErrors.INTERNAL.toString()) + child2.setGlobalErrorCode("ORG_ZSTACK_STORAGE_PRIMARY_10039") + child2.setFormatArgs(["child2-uuid"] as String[]) + + errorList.setCauses([child1, child2]) + + i18nService.localizeErrorCode(errorList, "zh_CN") + + assert errorList.getMessage().contains("list-uuid") + assert errorList.getCauses()[0].getMessage().contains("child1-uuid") + assert errorList.getCauses()[1].getMessage().contains("child2-uuid") + } + + void testRestServerSyncApiWithAcceptLanguage() { + // Intercept APIQueryZoneMsg and return error with known globalErrorCode + ErrorCode mockError = new ErrorCode() + mockError.setCode(SysErrors.INTERNAL.toString()) + mockError.setDetails("no primary storage[uuid:test-sync-uuid] exists") + mockError.setGlobalErrorCode("ORG_ZSTACK_STORAGE_PRIMARY_10039") + mockError.setFormatArgs(["test-sync-uuid"] as String[]) + + env.message(APIQueryZoneMsg.class) { APIQueryZoneMsg msg, bus -> + bus.replyErrorByMessageType(msg, mockError) + } + + try { + // Make raw HTTP GET to /v1/zones with Accept-Language: zh-CN + String sessionId = adminSession() + String url = "http://127.0.0.1:${WebBeanConstructor.port}/v1/zones" + HttpURLConnection conn = (HttpURLConnection) new URL(url).openConnection() + try { + conn.setRequestMethod("GET") + conn.setRequestProperty("Authorization", "OAuth ${sessionId}") + conn.setRequestProperty("Accept-Language", "zh-CN") + + int responseCode = conn.getResponseCode() + String responseBody + if (responseCode >= 400) { + responseBody = conn.getErrorStream()?.text ?: "" + } else { + responseBody = conn.getInputStream()?.text ?: "" + } + + assert responseCode == 503 // SERVICE_UNAVAILABLE for error responses + Map response = JSONObjectUtil.toObject(responseBody, LinkedHashMap.class) + Map error = response.get("error") as Map + assert error != null + assert error.get("message") != null + String message = error.get("message") as String + assert message.contains("未找到主存储") + assert message.contains("test-sync-uuid") + } finally { + conn.disconnect() + } + } finally { + env.cleanMessageHandlers() + } + } + + void testRestServerAsyncApiWithAcceptLanguage() { + // Use an async API (create zone) and intercept to return error + ErrorCode asyncError = new ErrorCode() + asyncError.setCode(SysErrors.INTERNAL.toString()) + asyncError.setDetails("no primary storage[uuid:test-async-uuid] exists") + asyncError.setGlobalErrorCode("ORG_ZSTACK_STORAGE_PRIMARY_10039") + asyncError.setFormatArgs(["test-async-uuid"] as String[]) + + env.message(org.zstack.header.zone.APICreateZoneMsg.class) { msg, bus -> + bus.replyErrorByMessageType(msg, asyncError) + } + + try { + // POST to create zone + String sessionId = adminSession() + String postUrl = "http://127.0.0.1:${WebBeanConstructor.port}/v1/zones" + String body = '{"params":{"name":"test-i18n-zone","description":"test"}}' + + HttpURLConnection postConn = (HttpURLConnection) new URL(postUrl).openConnection() + try { + postConn.setRequestMethod("POST") + postConn.setRequestProperty("Authorization", "OAuth ${sessionId}") + postConn.setRequestProperty("Accept-Language", "zh-CN") + postConn.setRequestProperty("Content-Type", "application/json") + postConn.setDoOutput(true) + postConn.getOutputStream().write(body.getBytes("UTF-8")) + + int postCode = postConn.getResponseCode() + String postBody + if (postCode >= 400) { + postBody = postConn.getErrorStream()?.text ?: "" + } else { + postBody = postConn.getInputStream()?.text ?: "" + } + + // Async API returns 202 with location header for job polling + if (postCode == 202) { + Map postResponse = JSONObjectUtil.toObject(postBody, LinkedHashMap.class) + String location = postResponse.get("location") as String + assert location != null + + // Poll the job with Accept-Language header + retryInSecs(5) { + String jobUrl = location.startsWith("http") ? location : "http://127.0.0.1:${WebBeanConstructor.port}${location}" + HttpURLConnection jobConn = (HttpURLConnection) new URL(jobUrl).openConnection() + try { + jobConn.setRequestMethod("GET") + jobConn.setRequestProperty("Authorization", "OAuth ${sessionId}") + jobConn.setRequestProperty("Accept-Language", "zh-CN") + + int jobCode = jobConn.getResponseCode() + String jobBody + if (jobCode >= 400) { + jobBody = jobConn.getErrorStream()?.text ?: "" + } else { + jobBody = jobConn.getInputStream()?.text ?: "" + } + + // Job should be done (503 for error) + assert jobCode == 503 + Map jobResponse = JSONObjectUtil.toObject(jobBody, LinkedHashMap.class) + Map jobError = jobResponse.get("error") as Map + assert jobError != null + String jobMessage = jobError.get("message") as String + assert jobMessage != null + assert jobMessage.contains("未找到主存储") + assert jobMessage.contains("test-async-uuid") + } finally { + jobConn.disconnect() + } + } + } else { + // If returned directly (e.g. 503), check the error + assert postCode == 503 + Map directResponse = JSONObjectUtil.toObject(postBody, LinkedHashMap.class) + Map directError = directResponse.get("error") as Map + assert directError != null + String directMessage = directError.get("message") as String + assert directMessage != null + assert directMessage.contains("未找到主存储") + } + } finally { + postConn.disconnect() + } + } finally { + env.cleanMessageHandlers() + } + } + + void testNoAcceptLanguageHeaderFallsBackToEnUS() { + // Intercept APIQueryZoneMsg and return error with known globalErrorCode + ErrorCode mockError = new ErrorCode() + mockError.setCode(SysErrors.INTERNAL.toString()) + mockError.setDetails("no primary storage[uuid:test-no-lang] exists") + mockError.setGlobalErrorCode("ORG_ZSTACK_STORAGE_PRIMARY_10039") + mockError.setFormatArgs(["test-no-lang"] as String[]) + + env.message(APIQueryZoneMsg.class) { APIQueryZoneMsg msg, bus -> + bus.replyErrorByMessageType(msg, mockError) + } + + try { + // Make raw HTTP GET without Accept-Language header + String sessionId = adminSession() + String url = "http://127.0.0.1:${WebBeanConstructor.port}/v1/zones" + HttpURLConnection conn = (HttpURLConnection) new URL(url).openConnection() + try { + conn.setRequestMethod("GET") + conn.setRequestProperty("Authorization", "OAuth ${sessionId}") + // No Accept-Language header + + int responseCode = conn.getResponseCode() + String responseBody + if (responseCode >= 400) { + responseBody = conn.getErrorStream()?.text ?: "" + } else { + responseBody = conn.getInputStream()?.text ?: "" + } + + assert responseCode == 503 + Map response = JSONObjectUtil.toObject(responseBody, LinkedHashMap.class) + Map error = response.get("error") as Map + assert error != null + // Without Accept-Language, resolveLocale defaults to en_US + // So message should be the en_US version + String message = error.get("message") as String + assert message != null + assert message.contains("no primary storage") + assert message.contains("test-no-lang") + } finally { + conn.disconnect() + } + } finally { + env.cleanMessageHandlers() + } + } +} diff --git a/test/src/test/groovy/org/zstack/test/integration/storage/StorageTest.groovy b/test/src/test/groovy/org/zstack/test/integration/storage/StorageTest.groovy index 56aadc40d11..46222aea395 100755 --- a/test/src/test/groovy/org/zstack/test/integration/storage/StorageTest.groovy +++ b/test/src/test/groovy/org/zstack/test/integration/storage/StorageTest.groovy @@ -23,6 +23,7 @@ class StorageTest extends Test { lb() portForwarding() expon() + xinfini() zbs() } diff --git a/test/src/test/groovy/org/zstack/test/integration/storage/primary/PrimaryStorageTest.groovy b/test/src/test/groovy/org/zstack/test/integration/storage/primary/PrimaryStorageTest.groovy index 75a789925a4..ee1c9309406 100755 --- a/test/src/test/groovy/org/zstack/test/integration/storage/primary/PrimaryStorageTest.groovy +++ b/test/src/test/groovy/org/zstack/test/integration/storage/primary/PrimaryStorageTest.groovy @@ -14,6 +14,8 @@ class PrimaryStorageTest extends Test { smp() ceph() externalPrimaryStorage() + expon() + xinfini() zbs() virtualRouter() vyos() diff --git a/test/src/test/groovy/org/zstack/test/integration/storage/primary/addon/ExternalPrimaryStorageSelectBackupStorageCase.groovy b/test/src/test/groovy/org/zstack/test/integration/storage/primary/addon/ExternalPrimaryStorageSelectBackupStorageCase.groovy new file mode 100644 index 00000000000..9419db12215 --- /dev/null +++ b/test/src/test/groovy/org/zstack/test/integration/storage/primary/addon/ExternalPrimaryStorageSelectBackupStorageCase.groovy @@ -0,0 +1,200 @@ +package org.zstack.test.integration.storage.primary.addon + +import org.zstack.core.Platform +import org.zstack.core.cloudbus.CloudBus +import org.zstack.core.db.DatabaseFacade +import org.zstack.core.db.SQL +import org.zstack.header.message.MessageReply +import org.zstack.header.storage.backup.BackupStorageEO +import org.zstack.header.storage.backup.BackupStorageState +import org.zstack.header.storage.backup.BackupStorageStatus +import org.zstack.header.storage.backup.BackupStorageZoneRefVO +import org.zstack.header.storage.backup.BackupStorageZoneRefVO_ +import org.zstack.header.storage.primary.PrimaryStorageConstant +import org.zstack.header.storage.primary.SelectBackupStorageMsg +import org.zstack.header.storage.primary.SelectBackupStorageReply +import org.zstack.sdk.PrimaryStorageInventory +import org.zstack.sdk.ZoneInventory +import org.zstack.test.integration.storage.StorageTest +import org.zstack.testlib.EnvSpec +import org.zstack.testlib.SubCase +import org.zstack.utils.data.SizeUnit + +/** + * ZSTAC-71706: ExternalPrimaryStorage backup storage selection in mixed environment. + * + * Bug: List.indexOf() returns -1 for types not in preferBsTypes, + * causing ascending sort to place non-preferred types (e.g. VCenterBackupStorage) + * before preferred types in the sorted result. + * + * Fix: Filter out non-preferred backup storage types before sorting by preference. + * + * This case sets up a ZBS ExternalPrimaryStorage (preferBsTypes = [ImageStoreBackupStorage]) + * with multiple backup storage types attached to the zone, then sends SelectBackupStorageMsg + * via CloudBus to verify the handler selects the correct preferred backup storage. + */ +class ExternalPrimaryStorageSelectBackupStorageCase extends SubCase { + EnvSpec env + ZoneInventory zone + PrimaryStorageInventory ps + DatabaseFacade dbf + CloudBus bus + List manualBsUuids = [] + + @Override + void clean() { + manualBsUuids.each { uuid -> + SQL.New(BackupStorageZoneRefVO.class) + .eq(BackupStorageZoneRefVO_.backupStorageUuid, uuid) + .hardDelete() + dbf.removeByPrimaryKey(uuid, BackupStorageEO.class) + } + env.delete() + } + + @Override + void setup() { + useSpring(StorageTest.springSpec) + } + + @Override + void environment() { + env = makeEnv { + sftpBackupStorage { + name = "sftp" + url = "/sftp" + username = "root" + password = "password" + hostname = "127.0.0.2" + + image { + name = "image" + url = "http://zstack.org/download/test.qcow2" + size = SizeUnit.GIGABYTE.toByte(1) + virtio = true + } + } + + zone { + name = "zone" + description = "test" + + cluster { + name = "cluster" + hypervisorType = "KVM" + + kvm { + name = "kvm" + managementIp = "127.0.0.1" + username = "root" + password = "password" + } + + 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" + } + } + } + + externalPrimaryStorage { + name = "zbs-ps" + identity = "zbs" + defaultOutputProtocol = "CBD" + config = '{"mdsUrls":["root:password@127.0.1.1","root:password@127.0.1.2","root:password@127.0.1.3"],"logicalPoolName":"lpool1"}' + url = "zbs" + } + + attachBackupStorage("sftp") + } + } + } + + @Override + void test() { + env.create { + zone = env.inventoryByName("zone") as ZoneInventory + ps = env.inventoryByName("zbs-ps") as PrimaryStorageInventory + dbf = bean(DatabaseFacade.class) + bus = bean(CloudBus.class) + + testErrorWhenNoPreferredTypeAvailable() + testSelectPreferredOverNonPreferred() + } + } + + /** + * When only non-preferred backup storage types exist in the zone, + * the selection should return an error (no matching preferred types). + * Zone has SftpBackupStorage (from env) and VCenterBackupStorage, + * neither of which is in zbs's preferBsTypes [ImageStoreBackupStorage]. + */ + void testErrorWhenNoPreferredTypeAvailable() { + createAndAttachBackupStorage("vcenter-bs", "VCenterBackupStorage") + + SelectBackupStorageMsg msg = new SelectBackupStorageMsg() + msg.setPrimaryStorageUuid(ps.uuid) + msg.setRequiredSize(SizeUnit.MEGABYTE.toByte(1)) + bus.makeTargetServiceIdByResourceUuid(msg, PrimaryStorageConstant.SERVICE_ID, ps.uuid) + MessageReply reply = bus.call(msg) + + assert !reply.isSuccess() : "Should fail when no preferred BS type is available" + } + + /** + * Reproduces ZSTAC-71706: zone has both ImageStoreBackupStorage (preferred) + * and VCenterBackupStorage (non-preferred, created in previous test). + * Before the fix, indexOf() returns -1 for VCenterBackupStorage causing + * it to sort before ImageStoreBackupStorage. After the fix, non-preferred + * types are filtered out entirely, and ImageStoreBackupStorage is correctly selected. + */ + void testSelectPreferredOverNonPreferred() { + createAndAttachBackupStorage("imagestore-bs", "ImageStoreBackupStorage") + + SelectBackupStorageMsg msg = new SelectBackupStorageMsg() + msg.setPrimaryStorageUuid(ps.uuid) + msg.setRequiredSize(SizeUnit.MEGABYTE.toByte(1)) + bus.makeTargetServiceIdByResourceUuid(msg, PrimaryStorageConstant.SERVICE_ID, ps.uuid) + MessageReply reply = bus.call(msg) + + assert reply.isSuccess() : "SelectBackupStorageMsg should succeed" + SelectBackupStorageReply bsReply = reply as SelectBackupStorageReply + assert bsReply.inventory != null + assert bsReply.inventory.type == "ImageStoreBackupStorage" : + "Should select preferred ImageStoreBackupStorage, but got ${bsReply.inventory.type}" + } + + private void createAndAttachBackupStorage(String name, String type) { + String uuid = Platform.getUuid() + + def bsEo = new BackupStorageEO() + bsEo.setUuid(uuid) + bsEo.setName(name) + bsEo.setType(type) + bsEo.setState(BackupStorageState.Enabled) + bsEo.setStatus(BackupStorageStatus.Connected) + bsEo.setTotalCapacity(SizeUnit.TERABYTE.toByte(100)) + bsEo.setAvailableCapacity(SizeUnit.TERABYTE.toByte(100)) + bsEo.setUrl("http://test-" + name) + dbf.persist(bsEo) + + def ref = new BackupStorageZoneRefVO() + ref.setBackupStorageUuid(uuid) + ref.setZoneUuid(zone.uuid) + dbf.persist(ref) + + manualBsUuids.add(uuid) + } +} diff --git a/test/src/test/groovy/org/zstack/test/integration/storage/primary/addon/expon/ExponPrimaryStorageCase.groovy b/test/src/test/groovy/org/zstack/test/integration/storage/primary/addon/expon/ExponPrimaryStorageCase.groovy index 0f1e52b24c0..9a8a9d0908c 100644 --- a/test/src/test/groovy/org/zstack/test/integration/storage/primary/addon/expon/ExponPrimaryStorageCase.groovy +++ b/test/src/test/groovy/org/zstack/test/integration/storage/primary/addon/expon/ExponPrimaryStorageCase.groovy @@ -1,6 +1,7 @@ package org.zstack.test.integration.storage.primary.addon.expon import org.springframework.http.HttpEntity +import javax.servlet.http.HttpServletRequest import org.zstack.compute.cluster.ClusterGlobalConfig import org.zstack.compute.vm.VmGlobalConfig import org.zstack.core.Platform @@ -71,7 +72,7 @@ class ExponPrimaryStorageCase extends SubCase { ExponStorageController controller ExponApiHelper apiHelper - String exponUrl = "https://admin:Admin123@172.25.108.64:443/pool" + String exponUrl = "http://admin:Admin123@127.0.0.1:8989/pool" String exportProtocol = "iscsi://" @Override @@ -171,11 +172,6 @@ class ExponPrimaryStorageCase extends SubCase { void test() { System.setProperty("useImageSpecSize", "true") env.create { - if (System.getProperty("inTestSuite") != null) { - logger.debug("skip expon case in test suite") - return - } - cluster = env.inventoryByName("cluster") as ClusterInventory instanceOffering = env.inventoryByName("instanceOffering") as InstanceOfferingInventory diskOffering = env.inventoryByName("diskOffering") as DiskOfferingInventory @@ -207,7 +203,6 @@ class ExponPrimaryStorageCase extends SubCase { reconnectPrimaryStorage { uuid = ps.uuid } - testCreateVmWhenSpecifiedSblk() testDeletePs() } } @@ -283,6 +278,21 @@ class ExponPrimaryStorageCase extends SubCase { assert r.success assert Q.New(PrimaryStorageHostRefVO.class).eq(PrimaryStorageHostRefVO_.hostUuid, host1.uuid).find().status.toString() == "Connected" + // override USS simulator to return empty for host2's vhost_127_0_0_3 + env.simulator("/api/v2/(sync/)?wds/uss") { HttpServletRequest req, HttpEntity e, EnvSpec spec -> + def nameParam = req.getParameter("name") + if (nameParam == "vhost_127_0_0_3") { + return [ret_code: "0", message: "", total: 0, uss_gateways: []] + } + def ussName = nameParam ?: "vhost_localhost" + return [ret_code: "0", message: "", total: 1, uss_gateways: [ + [id: "test-uss-" + ussName, name: ussName, type: "uss", status: "health", + tianshu_id: "test-tianshu-id", tianshu_name: "test-tianshu", + manager_ip: "127.0.0.1", business_port: 4420, business_network: "127.0.0.1/8", + create_time: System.currentTimeMillis(), update_time: System.currentTimeMillis()] + ]] + } + pmsg = new PingHostMsg() pmsg.hostUuid = host2.uuid bus.makeTargetServiceIdByResourceUuid(pmsg, HostConstant.SERVICE_ID, host2.uuid) @@ -307,7 +317,7 @@ class ExponPrimaryStorageCase extends SubCase { String pswd = "Pswd@#123" String encodePswd = URLEncoder.encode(pswd, "UTF-8") discoverExternalPrimaryStorage { - url = String.format("https://complex:%s@172.25.108.64:443/pool", encodePswd) + url = String.format("http://complex:%s@127.0.0.1:8989/pool", encodePswd) identity = "expon" } } @@ -338,7 +348,7 @@ class ExponPrimaryStorageCase extends SubCase { assert cmd.rootVolume.format == "raw" if (cmd.cdRoms != null) { cmd.cdRoms.forEach { - if (!it.isEmpty()) { + if (!it.isEmpty() && it.getPath() != null) { assert it.getPath().startsWith(exportProtocol) } } @@ -346,28 +356,15 @@ class ExponPrimaryStorageCase extends SubCase { return rsp } - // create vm concurrently - boolean success = false - Thread thread = new Thread(new Runnable() { - @Override - void run() { - def otherVm = createVmInstance { - name = "vm" - instanceOfferingUuid = instanceOffering.uuid - rootDiskOfferingUuid = diskOffering.uuid - imageUuid = image.uuid - l3NetworkUuids = [l3.uuid] - hostUuid = host1.uuid - } as VmInstanceInventory - - assert otherVm.allVolumes[0].size == diskOffering.diskSize - assert apiHelper.getVolume(getVolIdFromPath(otherVm.allVolumes[0].installPath)).volumeSize == diskOffering.diskSize - deleteVm(otherVm.uuid) - success = true - } - }) + def otherVm = createVmInstance { + name = "vm" + instanceOfferingUuid = instanceOffering.uuid + rootDiskOfferingUuid = diskOffering.uuid + imageUuid = image.uuid + l3NetworkUuids = [l3.uuid] + hostUuid = host1.uuid + } as VmInstanceInventory - thread.run() vm = createVmInstance { name = "vm" instanceOfferingUuid = instanceOffering.uuid @@ -376,8 +373,7 @@ class ExponPrimaryStorageCase extends SubCase { hostUuid = host1.uuid } as VmInstanceInventory - thread.join() - assert success + deleteVm(otherVm.uuid) stopVmInstance { uuid = vm.uuid @@ -683,8 +679,15 @@ class ExponPrimaryStorageCase extends SubCase { } void testClean() { + startVmInstance { + uuid = vm.uuid + hostUuid = host1.uuid + } + deleteVm(vm.uuid) + deleteVolume(vol2.uuid) + deleteDataVolume { uuid = vol.uuid } @@ -757,6 +760,9 @@ class ExponPrimaryStorageCase extends SubCase { primaryStorageUuidForRootVolume = sblk.uuid } as VmInstanceInventory + deleteVm(vm1.uuid) + deleteVm(vm2.uuid) + detachPrimaryStorageFromCluster { primaryStorageUuid = sblk.uuid clusterUuid = cluster.getUuid() @@ -775,8 +781,6 @@ class ExponPrimaryStorageCase extends SubCase { l3NetworkUuids = [l3.uuid] } as VmInstanceInventory - deleteVm(vm1.uuid) - deleteVm(vm2.uuid) deleteVm(vm3.uuid) } diff --git a/test/src/test/groovy/org/zstack/test/integration/storage/primary/addon/xinfini/XinfiniPrimaryStorageCase.groovy b/test/src/test/groovy/org/zstack/test/integration/storage/primary/addon/xinfini/XinfiniPrimaryStorageCase.groovy new file mode 100644 index 00000000000..fc924175afa --- /dev/null +++ b/test/src/test/groovy/org/zstack/test/integration/storage/primary/addon/xinfini/XinfiniPrimaryStorageCase.groovy @@ -0,0 +1,634 @@ +package org.zstack.test.integration.storage.primary.addon.xinfini + +import org.springframework.http.HttpEntity +import javax.servlet.http.HttpServletRequest +import org.zstack.compute.vm.VmGlobalConfig +import org.zstack.core.Platform +import org.zstack.core.cloudbus.CloudBus +import org.zstack.core.db.Q +import org.zstack.core.db.SQL +import org.zstack.xinfini.XInfiniStorageController +import org.zstack.xinfini.XInfiniApiHelper +import org.zstack.xinfini.XInfiniPathHelper +import org.zstack.xinfini.sdk.vhost.BdcModule +import org.zstack.xinfini.sdk.vhost.BdcBdevModule +import org.zstack.header.host.HostConstant +import org.zstack.header.host.PingHostMsg +import org.zstack.header.message.MessageReply +import org.zstack.header.storage.backup.DownloadImageFromRemoteTargetMsg +import org.zstack.header.storage.backup.DownloadImageFromRemoteTargetReply +import org.zstack.header.storage.backup.UploadImageToRemoteTargetReply +import org.zstack.header.storage.backup.UploadImageToRemoteTargetMsg +import org.zstack.header.storage.primary.ImageCacheShadowVO +import org.zstack.header.storage.primary.ImageCacheShadowVO_ +import org.zstack.header.storage.primary.ImageCacheVO +import org.zstack.header.storage.primary.ImageCacheVO_ +import org.zstack.header.storage.primary.PrimaryStorageHostRefVO +import org.zstack.header.storage.primary.PrimaryStorageHostRefVO_ +import org.zstack.header.vm.VmBootDevice +import org.zstack.header.vm.VmInstanceState +import org.zstack.header.vm.VmInstanceVO +import org.zstack.header.vm.VmInstanceVO_ +import org.zstack.header.vm.VmStateChangedOnHostMsg +import org.zstack.header.vm.devices.DeviceAddress +import org.zstack.header.vm.devices.VirtualDeviceInfo +import org.zstack.header.volume.VolumeVO +import org.zstack.header.volume.VolumeVO_ +import org.zstack.kvm.KVMAgentCommands +import org.zstack.kvm.KVMConstant +import org.zstack.kvm.KVMGlobalConfig +import org.zstack.kvm.VolumeTO +import org.zstack.sdk.* +import org.zstack.storage.addon.primary.ExternalPrimaryStorageFactory +import org.zstack.storage.backup.BackupStorageSystemTags +import org.zstack.tag.SystemTagCreator +import org.zstack.test.integration.storage.StorageTest +import org.zstack.testlib.EnvSpec +import org.zstack.testlib.SubCase +import org.zstack.utils.data.SizeUnit +import org.zstack.utils.gson.JSONObjectUtil + +import static java.util.Arrays.asList + +class XinfiniPrimaryStorageCase extends SubCase { + EnvSpec env + ClusterInventory cluster + InstanceOfferingInventory instanceOffering + DiskOfferingInventory diskOffering + ImageInventory image, iso + L3NetworkInventory l3 + PrimaryStorageInventory ps + BackupStorageInventory bs + VmInstanceInventory vm + VolumeInventory vol, vol2 + HostInventory host1, host2 + CloudBus bus + XInfiniStorageController controller + + String xinfiniUrl = "http://127.0.0.1:8989" + String xinfiniConfig = '{"token":"test-token","pools":[{"id":1,"name":"pool1"}],"nodes":[{"ip":"127.0.0.1","port":8989}]}' + String exportProtocol = "iscsi://" + + @Override + void clean() { + System.setProperty("useImageSpecSize", "false") + env.delete() + } + + @Override + void setup() { + useSpring(StorageTest.springSpec) + } + + @Override + void environment() { + env = makeEnv { + instanceOffering { + name = "instanceOffering" + memory = SizeUnit.GIGABYTE.toByte(8) + cpu = 4 + } + + diskOffering { + name = "diskOffering" + diskSize = SizeUnit.GIGABYTE.toByte(2) + } + + sftpBackupStorage { + name = "sftp" + url = "/sftp" + username = "root" + password = "password" + hostname = "127.0.0.2" + + image { + name = "image" + url = "http://zstack.org/download/test.qcow2" + size = SizeUnit.GIGABYTE.toByte(1) + virtio = true + } + + image { + name = "iso" + url = "http://zstack.org/download/test.iso" + size = SizeUnit.GIGABYTE.toByte(1) + format = "iso" + virtio = true + } + } + + zone { + name = "zone" + description = "test" + + cluster { + name = "cluster" + hypervisorType = "KVM" + + kvm { + name = "kvm" + managementIp = "localhost" + username = "root" + password = "password" + } + kvm { + name = "kvm2" + managementIp = "127.0.0.3" + username = "root" + password = "password" + } + + 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" + } + } + } + + attachBackupStorage("sftp") + } + } + } + + @Override + void test() { + System.setProperty("useImageSpecSize", "true") + env.create { + cluster = env.inventoryByName("cluster") as ClusterInventory + instanceOffering = env.inventoryByName("instanceOffering") as InstanceOfferingInventory + diskOffering = env.inventoryByName("diskOffering") as DiskOfferingInventory + image = env.inventoryByName("image") as ImageInventory + iso = env.inventoryByName("iso") as ImageInventory + l3 = env.inventoryByName("l3") as L3NetworkInventory + bs = env.inventoryByName("sftp") as BackupStorageInventory + host1 = env.inventoryByName("kvm") as HostInventory + host2 = env.inventoryByName("kvm2") as HostInventory + bus = bean(CloudBus.class) + + KVMGlobalConfig.VM_SYNC_ON_HOST_PING.updateValue(true) + simulatorEnv() + testCreateXinfiniStorage() + testCreateVm() + testHandleInactiveVolume() + testCreateVolumeRollback() + testAttachIso() + testCreateDataVolume() + testCreateSnapshot() + testCreateTemplate() + testClean() + testImageCacheClean() + testDeletePs() + } + } + + void simulatorEnv() { + env.afterSimulator(KVMConstant.KVM_ATTACH_VOLUME) { KVMAgentCommands.AttachDataVolumeResponse rsp, HttpEntity e -> + KVMAgentCommands.AttachDataVolumeCmd cmd = JSONObjectUtil.toObject(e.body, KVMAgentCommands.AttachDataVolumeCmd.class) + + VirtualDeviceInfo info = new VirtualDeviceInfo() + info.resourceUuid = cmd.volume.resourceUuid + info.deviceAddress = new DeviceAddress() + info.deviceAddress.domain = "0000" + info.deviceAddress.bus = "00" + info.deviceAddress.slot = Long.toHexString(Q.New(VolumeVO.class).eq(VolumeVO_.vmInstanceUuid, cmd.vmUuid).count()) + info.deviceAddress.function = "0" + + rsp.virtualDeviceInfoList = [] + rsp.virtualDeviceInfoList.addAll(info) + return rsp + } + + SystemTagCreator creator = BackupStorageSystemTags.ISCSI_INITIATOR_NAME.newSystemTagCreator(bs.uuid); + creator.setTagByTokens(Collections.singletonMap(BackupStorageSystemTags.ISCSI_INITIATOR_NAME_TOKEN, "iqn.1994-05.com.redhat:fc16b4d4fb3f")); + creator.inherent = false; + creator.recreate = true; + creator.create(); + } + + void testCreateXinfiniStorage() { + def zone = env.inventoryByName("zone") as ZoneInventory + + ps = addExternalPrimaryStorage { + name = "test" + zoneUuid = zone.uuid + url = xinfiniUrl + identity = "xinfini" + config = xinfiniConfig + defaultOutputProtocol = "Vhost" + } as ExternalPrimaryStorageInventory + + ps = queryPrimaryStorage {}[0] as ExternalPrimaryStorageInventory + assert ps.getAddonInfo() != null + + attachPrimaryStorageToCluster { + primaryStorageUuid = ps.uuid + clusterUuid = cluster.uuid + } + + ExternalPrimaryStorageFactory factory = Platform.getComponentLoader().getComponent(ExternalPrimaryStorageFactory.class) + controller = factory.getControllerSvc(ps.uuid) as XInfiniStorageController + + PingHostMsg pmsg = new PingHostMsg() + pmsg.hostUuid = host1.uuid + bus.makeTargetServiceIdByResourceUuid(pmsg, HostConstant.SERVICE_ID, host1.uuid) + MessageReply r = bus.call(pmsg) + assert r.success + assert Q.New(PrimaryStorageHostRefVO.class).eq(PrimaryStorageHostRefVO_.hostUuid, host1.uuid).find().status.toString() == "Connected" + + pmsg = new PingHostMsg() + pmsg.hostUuid = host2.uuid + bus.makeTargetServiceIdByResourceUuid(pmsg, HostConstant.SERVICE_ID, host2.uuid) + r = bus.call(pmsg) + assert r.success + assert Q.New(PrimaryStorageHostRefVO.class).eq(PrimaryStorageHostRefVO_.hostUuid, host2.uuid).find().status.toString() == "Connected" + + // ping again + pmsg = new PingHostMsg() + pmsg.hostUuid = host1.uuid + bus.makeTargetServiceIdByResourceUuid(pmsg, HostConstant.SERVICE_ID, host1.uuid) + r = bus.call(pmsg) + assert r.success + assert Q.New(PrimaryStorageHostRefVO.class).eq(PrimaryStorageHostRefVO_.hostUuid, host1.uuid).find().status.toString() == "Connected" + + reconnectPrimaryStorage { + uuid = ps.uuid + } + } + + void testCreateVm() { + def result = getCandidatePrimaryStoragesForCreatingVm { + l3NetworkUuids = [l3.uuid] + imageUuid = image.uuid + } as GetCandidatePrimaryStoragesForCreatingVmResult + + assert result.getRootVolumePrimaryStorages().size() == 1 + + env.message(UploadImageToRemoteTargetMsg.class) { UploadImageToRemoteTargetMsg msg, CloudBus bus -> + UploadImageToRemoteTargetReply r = new UploadImageToRemoteTargetReply() + assert msg.getRemoteTargetUrl().startsWith(exportProtocol) + assert msg.getFormat() == "raw" + bus.reply(msg, r) + } + + env.afterSimulator(KVMConstant.KVM_START_VM_PATH) { rsp, HttpEntity e -> + def cmd = JSONObjectUtil.toObject(e.body, KVMAgentCommands.StartVmCmd.class) + assert cmd.rootVolume.deviceType == VolumeTO.VHOST + assert cmd.rootVolume.installPath.startsWith("/var/run/bdc-") + assert cmd.rootVolume.format == "raw" + if (cmd.cdRoms != null) { + cmd.cdRoms.forEach { + if (!it.isEmpty() && it.getPath() != null) { + assert it.getPath().startsWith(exportProtocol) + } + } + } + return rsp + } + + def otherVm = createVmInstance { + name = "vm" + instanceOfferingUuid = instanceOffering.uuid + rootDiskOfferingUuid = diskOffering.uuid + imageUuid = image.uuid + l3NetworkUuids = [l3.uuid] + hostUuid = host1.uuid + } as VmInstanceInventory + + assert otherVm.allVolumes[0].size == diskOffering.diskSize + + vm = createVmInstance { + name = "vm" + instanceOfferingUuid = instanceOffering.uuid + imageUuid = image.uuid + l3NetworkUuids = [l3.uuid] + hostUuid = host1.uuid + } as VmInstanceInventory + + deleteVm(otherVm.uuid) + + stopVmInstance { + uuid = vm.uuid + } + + startVmInstance { + uuid = vm.uuid + hostUuid = host1.uuid + } + + rebootVmInstance { + uuid = vm.uuid + } + + def vm2 = createVmInstance { + name = "vm" + instanceOfferingUuid = instanceOffering.uuid + rootDiskOfferingUuid = diskOffering.uuid + imageUuid = iso.uuid + l3NetworkUuids = [l3.uuid] + hostUuid = host1.uuid + } as VmInstanceInventory + + deleteVm(vm2.uuid) + } + + void testHandleInactiveVolume() { + def rootVolInstallPath = Q.New(VolumeVO.class).eq(VolumeVO_.uuid, vm.rootVolumeUuid).select(VolumeVO_.installPath).findValue() + int volId = XInfiniPathHelper.getVolIdFromPath(rootVolInstallPath as String) + BdcModule bdc = controller.apiHelper.queryBdcByIp(host1.managementIp) + BdcBdevModule bdev = controller.apiHelper.queryBdcBdevByVolumeIdAndBdcId(volId, bdc.spec.id) + assert bdev != null + + def clusterUuid = controller.apiHelper.getClusterUuid() + def vhostSocketDir = "/var/run/bdc-${clusterUuid}/" + + env.simulator(KVMConstant.KVM_VOLUME_SYNC_PATH) { HttpEntity e -> + def cmd = JSONObjectUtil.toObject(e.body, KVMAgentCommands.VolumeSyncCmd.class) + assert cmd.storagePaths.get(0).endsWith("/volume-*") + def rsp = new KVMAgentCommands.VolumeSyncRsp() + rsp.inactiveVolumePaths = new HashMap<>() + rsp.inactiveVolumePaths.put(cmd.storagePaths.get(0), asList("${vhostSocketDir}volume-${vm.rootVolumeUuid}" as String)) + return rsp + } + + def msg = new PingHostMsg() + msg.hostUuid = host1.uuid + bus.makeTargetServiceIdByResourceUuid(msg, HostConstant.SERVICE_ID, host1.uuid) + MessageReply r = bus.call(msg) + assert r.success + + sleep(1000) + // vm in running, not deactivate volume + bdev = controller.apiHelper.queryBdcBdevByVolumeIdAndBdcId(volId, bdc.spec.id) + assert bdev != null + + SQL.New(VmInstanceVO.class).eq(VmInstanceVO_.uuid, vm.uuid).set(VmInstanceVO_.hostUuid, null).set(VmInstanceVO_.state, VmInstanceState.Starting).update() + env.message(VmStateChangedOnHostMsg.class) { VmStateChangedOnHostMsg cmsg, CloudBus bus -> + bus.reply(cmsg, new MessageReply()) + } + r = bus.call(msg) + assert r.success + + sleep(1000) + // vm in starting, not deactivate volume + bdev = controller.apiHelper.queryBdcBdevByVolumeIdAndBdcId(volId, bdc.spec.id) + assert bdev != null + env.cleanMessageHandlers() + + SQL.New(VmInstanceVO.class).eq(VmInstanceVO_.uuid, vm.uuid).set(VmInstanceVO_.hostUuid, null).set(VmInstanceVO_.state, VmInstanceState.Stopped).update() + r = bus.call(msg) + assert r.success + + sleep(1000) + // vm in stop, deactivate volume + retryInSecs { + bdev = controller.apiHelper.queryBdcBdevByVolumeIdAndBdcId(volId, bdc.spec.id) + assert bdev == null + } + + SQL.New(VmInstanceVO.class).eq(VmInstanceVO_.uuid, vm.uuid).set(VmInstanceVO_.hostUuid, host1.uuid).set(VmInstanceVO_.state, VmInstanceState.Running).update() + env.simulator(KVMConstant.KVM_VOLUME_SYNC_PATH) { HttpEntity e -> + def cmd = JSONObjectUtil.toObject(e.body, KVMAgentCommands.VolumeSyncCmd.class) + assert cmd.storagePaths.get(0).endsWith("/volume-*") + return new KVMAgentCommands.VolumeSyncRsp() + } + } + + void testCreateVolumeRollback() { + def vol = createDataVolume { + name = "test" + diskOfferingUuid = diskOffering.uuid + primaryStorageUuid = ps.uuid + } as VolumeInventory + + env.afterSimulator(KVMConstant.KVM_ATTACH_VOLUME) { rsp, HttpEntity e -> + rsp.setError("on purpose") + return rsp + } + + expectError { + attachDataVolumeToVm { + vmInstanceUuid = vm.uuid + volumeUuid = vol.uuid + } + } + + env.afterSimulator(KVMConstant.KVM_ATTACH_VOLUME) { rsp, HttpEntity e -> + return rsp + } + + deleteVolume(vol.uuid) + } + + void testAttachIso() { + env.afterSimulator(KVMConstant.KVM_ATTACH_ISO_PATH) { rsp, HttpEntity e -> + def cmd = JSONObjectUtil.toObject(e.body, KVMAgentCommands.AttachIsoCmd.class) + assert cmd.iso.getPath().startsWith(exportProtocol) + return rsp + } + + attachIsoToVmInstance { + vmInstanceUuid = vm.uuid + isoUuid = iso.uuid + } + + rebootVmInstance { + uuid = vm.uuid + } + + setVmBootOrder { + uuid = vm.uuid + bootOrder = asList(VmBootDevice.CdRom.toString(), VmBootDevice.HardDisk.toString(), VmBootDevice.Network.toString()) + } + + stopVmInstance { + uuid = vm.uuid + } + + startVmInstance { + uuid = vm.uuid + } + + setVmBootOrder { + uuid = vm.uuid + bootOrder = asList(VmBootDevice.HardDisk.toString(), VmBootDevice.CdRom.toString(), VmBootDevice.Network.toString()) + } + + stopVmInstance { + uuid = vm.uuid + } + + startVmInstance { + uuid = vm.uuid + hostUuid = host1.uuid + } + + detachIsoFromVmInstance { + vmInstanceUuid = vm.uuid + } + } + + void testCreateDataVolume() { + vol = createDataVolume { + name = "test" + diskOfferingUuid = diskOffering.uuid + primaryStorageUuid = ps.uuid + } as VolumeInventory + + deleteVolume(vol.uuid) + + vol = createDataVolume { + name = "test" + diskOfferingUuid = diskOffering.uuid + primaryStorageUuid = ps.uuid + } as VolumeInventory + + attachDataVolumeToVm { + vmInstanceUuid = vm.uuid + volumeUuid = vol.uuid + } + + vol2 = createDataVolume { + name = "test" + diskOfferingUuid = diskOffering.uuid + } as VolumeInventory + + attachDataVolumeToVm { + vmInstanceUuid = vm.uuid + volumeUuid = vol2.uuid + } + + detachDataVolumeFromVm { + uuid = vol2.uuid + } + + attachDataVolumeToVm { + vmInstanceUuid = vm.uuid + volumeUuid = vol2.uuid + } + } + + void testCreateSnapshot() { + def snapshot = createVolumeSnapshot { + name = "test" + volumeUuid = vol.uuid + } as VolumeSnapshotInventory + + stopVmInstance { + uuid = vm.uuid + } + + revertVolumeFromSnapshot { + uuid = snapshot.uuid + } + + deleteVolumeSnapshot { + uuid = snapshot.uuid + } + + startVmInstance { + uuid = vm.uuid + } + } + + void testCreateTemplate() { + env.message(DownloadImageFromRemoteTargetMsg.class) { DownloadImageFromRemoteTargetMsg msg, CloudBus bus -> + DownloadImageFromRemoteTargetReply r = new DownloadImageFromRemoteTargetReply() + assert msg.getRemoteTargetUrl().startsWith(exportProtocol) + r.setInstallPath("zstore://test/image") + r.setSize(100L) + bus.reply(msg, r) + } + + def dataImage = createDataVolumeTemplateFromVolume { + name = "vol-image" + volumeUuid = vol.uuid + backupStorageUuids = [bs.uuid] + } as ImageInventory + + stopVmInstance { + uuid = vm.uuid + } + + def rootImage = createRootVolumeTemplateFromRootVolume { + name = "root-image" + rootVolumeUuid = vm.rootVolumeUuid + backupStorageUuids = [bs.uuid] + } as ImageInventory + } + + void testClean() { + deleteVm(vm.uuid) + + deleteVolume(vol2.uuid) + + deleteDataVolume { + uuid = vol.uuid + } + + expungeDataVolume { + uuid = vol.uuid + } + } + + void testImageCacheClean() { + deleteImage { + uuid = image.uuid + } + + expungeImage { + imageUuid = image.uuid + } + + cleanUpImageCacheOnPrimaryStorage { + uuid = ps.uuid + } + + retryInSecs { + assert Q.New(ImageCacheVO.class).eq(ImageCacheVO_.imageUuid, image.uuid).count() == 0 + assert Q.New(ImageCacheShadowVO.class).eq(ImageCacheShadowVO_.imageUuid, image.uuid).count() == 0 + } + } + + void testDeletePs() { + detachPrimaryStorageFromCluster { + primaryStorageUuid = ps.uuid + clusterUuid = cluster.uuid + } + + deletePrimaryStorage { + uuid = ps.uuid + } + } + + void deleteVm(String vmUuid) { + destroyVmInstance { + uuid = vmUuid + } + + expungeVmInstance { + uuid = vmUuid + } + } + + void deleteVolume(String volUuid) { + deleteDataVolume { + uuid = volUuid + } + + expungeDataVolume { + uuid = volUuid + } + } +} 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 new file mode 100644 index 00000000000..917cf44cbc2 --- /dev/null +++ b/test/src/test/java/org/zstack/test/core/errorcode/TestGlobalErrorCodeI18n.java @@ -0,0 +1,154 @@ +package org.zstack.test.core.errorcode; + +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; +import org.zstack.core.componentloader.ComponentLoader; +import org.zstack.core.errorcode.GlobalErrorCodeI18nService; +import org.zstack.header.errorcode.ErrorCode; +import org.zstack.header.errorcode.ErrorCodeList; +import org.zstack.test.BeanConstructor; +import org.zstack.utils.Utils; +import org.zstack.utils.logging.CLogger; + +import java.util.Arrays; + +public class TestGlobalErrorCodeI18n { + CLogger logger = Utils.getLogger(TestGlobalErrorCodeI18n.class); + ComponentLoader loader; + GlobalErrorCodeI18nService i18nService; + + @Before + public void setUp() throws Exception { + BeanConstructor con = new BeanConstructor(); + loader = con.build(); + i18nService = loader.getComponent(GlobalErrorCodeI18nService.class); + } + + @Test + public void testLoadJsonFiles() { + Assert.assertTrue("should load at least 2 locales", + i18nService.getAvailableLocales().size() >= 2); + logger.debug(String.format("loaded locales: %s", i18nService.getAvailableLocales())); + } + + @Test + public void testZhCNMatch() { + String msg = i18nService.getLocalizedMessage( + "ORG_ZSTACK_STORAGE_PRIMARY_10039", "zh_CN", new String[]{"abc123"}); + Assert.assertNotNull(msg); + Assert.assertTrue("should contain Chinese text", msg.contains("abc123")); + logger.debug(String.format("zh_CN message: %s", msg)); + } + + @Test + public void testEnUSMatch() { + String msg = i18nService.getLocalizedMessage( + "ORG_ZSTACK_STORAGE_PRIMARY_10039", "en_US", new String[]{"abc123"}); + Assert.assertNotNull(msg); + Assert.assertTrue("should contain English text", msg.contains("abc123")); + logger.debug(String.format("en_US message: %s", msg)); + } + + @Test + public void testFallbackToEnUS() { + String msg = i18nService.getLocalizedMessage( + "ORG_ZSTACK_STORAGE_PRIMARY_10039", "pt_BR", new String[]{"abc123"}); + Assert.assertNotNull("should fallback to en_US", msg); + logger.debug(String.format("fallback message: %s", msg)); + } + + @Test + public void testNonExistentGlobalErrorCode() { + String msg = i18nService.getLocalizedMessage( + "NOT_EXIST_CODE", "zh_CN", null); + Assert.assertNull("should return null for non-existent code", msg); + } + + @Test + public void testFormatArgsNull() { + String msg = i18nService.getLocalizedMessage( + "ORG_ZSTACK_STORAGE_PRIMARY_10039", "zh_CN", null); + Assert.assertNotNull(msg); + // template contains %s but args is null, should return raw template + Assert.assertTrue("should contain %s placeholder", msg.contains("%s")); + } + + @Test + public void testFormatArgsMismatch() { + // template has 1 %s but we pass 3 args - should not crash + String msg = i18nService.getLocalizedMessage( + "ORG_ZSTACK_STORAGE_PRIMARY_10039", "zh_CN", + new String[]{"arg1", "arg2", "arg3"}); + Assert.assertNotNull(msg); + } + + @Test + public void testLocalizeErrorCodeWithCause() { + ErrorCode cause = new ErrorCode(); + cause.setGlobalErrorCode("ORG_ZSTACK_STORAGE_PRIMARY_10039"); + cause.setFormatArgs(new String[]{"inner-uuid"}); + + ErrorCode error = new ErrorCode(); + error.setGlobalErrorCode("ORG_ZSTACK_STORAGE_PRIMARY_10039"); + error.setFormatArgs(new String[]{"outer-uuid"}); + error.setCause(cause); + + i18nService.localizeErrorCode(error, "zh_CN"); + + Assert.assertNotNull("error.message should be set", error.getMessage()); + Assert.assertNotNull("cause.message should be set", cause.getMessage()); + Assert.assertTrue(error.getMessage().contains("outer-uuid")); + Assert.assertTrue(cause.getMessage().contains("inner-uuid")); + } + + @Test + public void testLocalizeErrorCodeList() { + ErrorCodeList errorList = new ErrorCodeList(); + errorList.setCode("SYS.1000"); + + ErrorCode cause1 = new ErrorCode(); + cause1.setGlobalErrorCode("ORG_ZSTACK_STORAGE_PRIMARY_10039"); + cause1.setFormatArgs(new String[]{"uuid1"}); + + ErrorCode cause2 = new ErrorCode(); + cause2.setGlobalErrorCode("ORG_ZSTACK_STORAGE_PRIMARY_10039"); + cause2.setFormatArgs(new String[]{"uuid2"}); + + errorList.setCauses(Arrays.asList(cause1, cause2)); + + i18nService.localizeErrorCode(errorList, "zh_CN"); + + Assert.assertNotNull("cause1.message should be set", cause1.getMessage()); + Assert.assertNotNull("cause2.message should be set", cause2.getMessage()); + Assert.assertTrue(cause1.getMessage().contains("uuid1")); + Assert.assertTrue(cause2.getMessage().contains("uuid2")); + } + + @Test + public void testNoGlobalErrorCode() { + ErrorCode error = new ErrorCode("SYS.1000", "test error"); + // no globalErrorCode set — message should fall back to description + i18nService.localizeErrorCode(error, "zh_CN"); + 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/test/src/test/java/org/zstack/test/core/errorcode/TestLocaleUtils.java b/test/src/test/java/org/zstack/test/core/errorcode/TestLocaleUtils.java new file mode 100644 index 00000000000..f7d1e6fc821 --- /dev/null +++ b/test/src/test/java/org/zstack/test/core/errorcode/TestLocaleUtils.java @@ -0,0 +1,67 @@ +package org.zstack.test.core.errorcode; + +import org.junit.Assert; +import org.junit.Test; +import org.zstack.core.errorcode.LocaleUtils; + +import java.util.HashSet; +import java.util.Set; + +public class TestLocaleUtils { + private static final Set AVAILABLE_LOCALES = new HashSet<>(); + + static { + AVAILABLE_LOCALES.add("zh_CN"); + AVAILABLE_LOCALES.add("en_US"); + AVAILABLE_LOCALES.add("ja-JP"); + AVAILABLE_LOCALES.add("ko-KR"); + AVAILABLE_LOCALES.add("de-DE"); + AVAILABLE_LOCALES.add("fr-FR"); + AVAILABLE_LOCALES.add("ru-RU"); + AVAILABLE_LOCALES.add("th-TH"); + AVAILABLE_LOCALES.add("id-ID"); + AVAILABLE_LOCALES.add("zh_TW"); + } + + @Test + public void testSimpleLocale() { + String result = LocaleUtils.resolveLocale("zh-CN", AVAILABLE_LOCALES); + Assert.assertEquals("zh_CN", result); + } + + @Test + public void testMultiLocaleWithQuality() { + String result = LocaleUtils.resolveLocale("ja-JP,zh-CN;q=0.9,en;q=0.8", AVAILABLE_LOCALES); + Assert.assertEquals("ja-JP", result); + } + + @Test + public void testLanguageCodeOnly() { + String result = LocaleUtils.resolveLocale("zh", AVAILABLE_LOCALES); + Assert.assertEquals("zh_CN", result); + } + + @Test + public void testUnsupportedLocale() { + String result = LocaleUtils.resolveLocale("pt-BR", AVAILABLE_LOCALES); + Assert.assertEquals("en_US", result); + } + + @Test + public void testNullAndEmpty() { + Assert.assertEquals("en_US", LocaleUtils.resolveLocale(null, AVAILABLE_LOCALES)); + Assert.assertEquals("en_US", LocaleUtils.resolveLocale("", AVAILABLE_LOCALES)); + } + + @Test + public void testEnUS() { + String result = LocaleUtils.resolveLocale("en-US", AVAILABLE_LOCALES); + Assert.assertEquals("en_US", result); + } + + @Test + public void testUnderscoreFormat() { + String result = LocaleUtils.resolveLocale("zh_CN", AVAILABLE_LOCALES); + Assert.assertEquals("zh_CN", result); + } +} diff --git a/test/src/test/resources/zstack.properties b/test/src/test/resources/zstack.properties index f7f4da51bf3..42fa9e4f1ef 100755 --- a/test/src/test/resources/zstack.properties +++ b/test/src/test/resources/zstack.properties @@ -45,6 +45,7 @@ Zdfs.agentPort=8989 ApiMediator.apiWorkerNum=50 unitTestOn=true +elaboration.slowThresholdMs=200 exitJVMOnStop=false #CloudBus.closeTracker=true diff --git a/testlib/src/main/java/org/zstack/testlib/ApiHelper.groovy b/testlib/src/main/java/org/zstack/testlib/ApiHelper.groovy index f8470e35bd3..8a8f2b91416 100644 --- a/testlib/src/main/java/org/zstack/testlib/ApiHelper.groovy +++ b/testlib/src/main/java/org/zstack/testlib/ApiHelper.groovy @@ -1016,6 +1016,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 addBlockPrimaryStorage(@DelegatesTo(strategy = Closure.OWNER_FIRST, value = org.zstack.sdk.AddBlockPrimaryStorageAction.class) Closure c) { def a = new org.zstack.sdk.AddBlockPrimaryStorageAction() 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 @@ -30778,6 +30859,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 @@ -44154,6 +44264,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 @@ -56650,6 +56787,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/ExternalPrimaryStorageSpec.groovy b/testlib/src/main/java/org/zstack/testlib/ExternalPrimaryStorageSpec.groovy index ef70f3e7ad3..41a4bdb9e4e 100644 --- a/testlib/src/main/java/org/zstack/testlib/ExternalPrimaryStorageSpec.groovy +++ b/testlib/src/main/java/org/zstack/testlib/ExternalPrimaryStorageSpec.groovy @@ -2,6 +2,7 @@ package org.zstack.testlib import org.springframework.http.HttpEntity import org.zstack.core.db.DatabaseFacade +import org.zstack.core.Platform import org.zstack.kvm.KVMAgentCommands import org.zstack.storage.zbs.LogicalPoolInfo import org.zstack.cbd.kvm.KvmCbdCommands @@ -13,7 +14,9 @@ import org.zstack.utils.data.SizeUnit import org.zstack.utils.logging.CLogger import org.zstack.utils.gson.JSONObjectUtil +import javax.servlet.http.HttpServletRequest import java.util.concurrent.ConcurrentHashMap +import java.util.concurrent.atomic.AtomicInteger /** * @author Xingwei Yu @@ -264,6 +267,1221 @@ class ExternalPrimaryStorageSpec extends PrimaryStorageSpec { } } + static class ExponSimulators implements Simulator { + static final long TOTAL_CAPACITY = SizeUnit.TERABYTE.toByte(2) + static final long AVAILABLE_CAPACITY = SizeUnit.TERABYTE.toByte(1) + static final String POOL_ID = "test-pool-id-001" + static final String POOL_NAME = "pool" + static final String TIANSHU_ID = "test-tianshu-id-001" + static final String TIANSHU_NAME = "tianshu" + + static ConcurrentHashMap volumes = new ConcurrentHashMap<>() + static ConcurrentHashMap snapshots = new ConcurrentHashMap<>() + static Set vhostBoundUss = ConcurrentHashMap.newKeySet() + static ConcurrentHashMap vhostNameToId = new ConcurrentHashMap<>() + // Track iSCSI client group to snapshot mappings: clientId -> Set + static ConcurrentHashMap> iscsiClientSnapshots = new ConcurrentHashMap<>() + static AtomicInteger volumeCounter = new AtomicInteger(0) + static AtomicInteger snapshotCounter = new AtomicInteger(0) + + static void clear() { + volumes.clear() + snapshots.clear() + vhostBoundUss.clear() + vhostNameToId.clear() + iscsiClientSnapshots.clear() + volumeCounter.set(0) + snapshotCounter.set(0) + } + + @Override + void registerSimulators(EnvSpec espec) { + def simulator = { arg1, arg2 -> + espec.simulator(arg1, arg2) + } + + // Login: POST /api/v1/login + simulator("/api/v1/login") { + return [ret_code: "0", message: "", access_token: "test-session-token", refresh_token: "test-refresh-token", token_type: "Bearer"] + } + + // Logout: POST /api/v1/v2/logout + simulator("/api/v1/v2/logout") { + return [ret_code: "0", message: ""] + } + + // Task status: GET /api/v1/tasks/{id} + simulator("/api/v1/tasks/.*") { + return [ret_code: "0", message: "", status: "SUCCESS", ret_msg: "", progress: 100, id: "test-task-id"] + } + + // Query pools (QueryFailureDomainRequest): GET /api/v2/failure_domain + simulator("/api/v2/failure_domain") { + return [ret_code: "0", message: "", total: 1, failure_domains: [ + [id: POOL_ID, failure_domain_name: POOL_NAME, valid_size: TOTAL_CAPACITY, + real_data_size: TOTAL_CAPACITY - AVAILABLE_CAPACITY, raw_size: TOTAL_CAPACITY * 3, + data_size: TOTAL_CAPACITY - AVAILABLE_CAPACITY, redundancy_ploy: "replicated", + replicate_size: 3, health_status: "health", run_status: "normal", + tianshu_id: TIANSHU_ID, tianshu_name: TIANSHU_NAME, + create_time: System.currentTimeMillis(), update_time: System.currentTimeMillis()] + ]] + } + + // Get pool detail (GetFailureDomainRequest): GET /api/v2/failure_domain/{id} + simulator("/api/v2/failure_domain/[^/]+") { HttpServletRequest req, HttpEntity e, EnvSpec spec -> + return [ret_code: "0", message: "", members: [ + id: POOL_ID, failure_domain_name: POOL_NAME, valid_size: TOTAL_CAPACITY, + real_data_size: TOTAL_CAPACITY - AVAILABLE_CAPACITY, raw_size: TOTAL_CAPACITY * 3, + data_size: TOTAL_CAPACITY - AVAILABLE_CAPACITY, redundancy_ploy: "replicated", + replicate_size: 3, health_status: "health", run_status: "normal", + tianshu_id: TIANSHU_ID, tianshu_name: TIANSHU_NAME, + create_time: System.currentTimeMillis(), update_time: System.currentTimeMillis() + ]] + } + + // Get blacklist (GetFailureDomainBlacklistRequest): GET /api/v2/failure_domain/black_list/{id} + simulator("/api/v2/failure_domain/black_list/[^/]+") { HttpServletRequest req, HttpEntity e, EnvSpec spec -> + return [ret_code: "0", message: "", entries: []] + } + + // Clear blacklist: PUT /api/v2/failure_domain/black_list/clean + simulator("/api/v2/failure_domain/black_list/clean") { + return [ret_code: "0", message: ""] + } + + // Add volume path to blacklist: PUT /api/v2/failure_domain/black_list + simulator("/api/v2/failure_domain/black_list") { + return [ret_code: "0", message: ""] + } + + // Query clusters (QueryTianshuClusterRequest): GET /api/v2/tianshu + simulator("/api/v2/tianshu") { + return [ret_code: "0", message: "", total: 1, result: [ + [id: TIANSHU_ID, name: TIANSHU_NAME, health_status: "health", run_status: "normal", + create_time: System.currentTimeMillis(), update_time: System.currentTimeMillis()] + ]] + } + + // Query iSCSI targets (QueryIscsiTargetRequest): GET /api/v2/block/iscsi/gateways + // also matches sync variant + simulator("/api/v2/(sync/)?block/iscsi/gateways") { HttpServletRequest req, HttpEntity e, EnvSpec spec -> + def targetName = req.getParameter("name") ?: "iscsi-target-default" + def targetId = "test-iscsi-target-" + targetName + return [ret_code: "0", message: "", total: 1, gateways: [ + [id: targetId, name: targetName, status: "health", port: 3260, + iqn: "iqn.2022-07.com.expontech.wds:" + targetId, + tianshu_id: TIANSHU_ID, tianshu_name: TIANSHU_NAME, + create_time: System.currentTimeMillis(), update_time: System.currentTimeMillis()] + ]] + } + + // Create iSCSI target: POST /api/v2/sync/block/iscsi/gateways + simulator("/api/v2/sync/block/iscsi/gateways") { HttpEntity e -> + def body = JSONObjectUtil.toObject(e.body, LinkedHashMap.class) + def targetId = Platform.getUuid() + def targetName = body?.name ?: "iscsi-target-" + targetId + return [ret_code: "0", message: "", id: targetId, name: targetName] + } + + // iSCSI target operations with id: GET/DELETE/PUT /api/v2/[sync/]block/iscsi/gateways/{id}/... + simulator("/api/v2/(sync/)?block/iscsi/gateways/[^/]+(/.*)?") { HttpServletRequest req, HttpEntity e, EnvSpec spec -> + def uri = req.getRequestURI() + def matcher = (uri =~ /\/block\/iscsi\/gateways\/([^\/]+)/) + def targetId = matcher ? matcher[0][1] : Platform.getUuid() + def targetName = targetId.startsWith("test-iscsi-target-") ? targetId.substring("test-iscsi-target-".length()) : targetId + + return [ret_code: "0", message: "", + id: targetId, name: targetName, + iqn: "iqn.2022-07.com.expontech.wds:" + targetId, + port: 3260, lun_count: 0, + total: 0, gateways: [], + nodes: [ + [gateway_ip: "127.0.0.1", manager_ip: "localhost", name: "localhost", + server_id: Platform.getUuid(), tianshu_id: TIANSHU_ID, + uss_gw_id: "test-uss-vhost_localhost", uss_name: "iscsi_zstack"] + ], + server: []] + } + + // Query iSCSI client groups: GET /api/v2/block/iscsi/clients + simulator("/api/v2/(sync/)?block/iscsi/clients") { HttpServletRequest req, HttpEntity e, EnvSpec spec -> + def nameParam = req.getParameter("name") + if (nameParam != null) { + // Query by name + def clientId = "test-iscsi-client-" + nameParam + int snapNum = iscsiClientSnapshots.getOrDefault(clientId, Collections.emptySet()).size() + return [ret_code: "0", message: "", total: 1, clients: [ + [id: clientId, name: nameParam, status: "health", hosts: [], + iscsi_gw_count: 0, snap_num: snapNum, + create_time: System.currentTimeMillis(), update_time: System.currentTimeMillis()] + ]] + } + // Query all - return all tracked clients with snapshots + def allClients = iscsiClientSnapshots.collect { cId, snaps -> + [id: cId, name: cId, status: "health", hosts: [], + iscsi_gw_count: 0, snap_num: snaps.size(), + create_time: System.currentTimeMillis(), update_time: System.currentTimeMillis()] + } + if (allClients.isEmpty()) { + allClients = [[id: "test-iscsi-client-default", name: "iscsi-client-default", status: "health", hosts: [], + iscsi_gw_count: 0, snap_num: 0, + create_time: System.currentTimeMillis(), update_time: System.currentTimeMillis()]] + } + return [ret_code: "0", message: "", total: allClients.size(), clients: allClients] + } + + // Create iSCSI client group: POST /api/v2/sync/block/iscsi/clients + simulator("/api/v2/sync/block/iscsi/clients") { + def clientId = Platform.getUuid() + return [ret_code: "0", message: "", id: clientId] + } + + // iSCSI client group operations with id (including snapshot attachment) + simulator("/api/v2/(sync/)?block/iscsi/clients/[^/]+(/.*)?") { HttpServletRequest req, HttpEntity e, EnvSpec spec -> + def uri = req.getRequestURI() + def matcher = (uri =~ /\/block\/iscsi\/clients\/([^\/]+)/) + def clientId = matcher ? matcher[0][1] : "" + + // Handle snapshot add/remove: PUT /block/iscsi/clients/{id}/snapshots + if (uri.contains("/snapshots") && "PUT".equalsIgnoreCase(req.getMethod())) { + def body = JSONObjectUtil.toObject(e.body, LinkedHashMap.class) + def action = body?.action + def luns = body?.luns ?: [] + luns.each { lun -> + def snapId = lun?.id ?: lun?.lun_id + if (snapId != null) { + if ("add".equals(action)) { + iscsiClientSnapshots.computeIfAbsent(clientId, { ConcurrentHashMap.newKeySet() }).add(snapId.toString()) + } else if ("remove".equals(action)) { + iscsiClientSnapshots.getOrDefault(clientId, Collections.emptySet()).remove(snapId.toString()) + } + } + } + return [ret_code: "0", message: ""] + } + + // Handle snapshot query: GET /block/iscsi/clients/{id}/snapshots + if (uri.contains("/snapshots") && "GET".equalsIgnoreCase(req.getMethod())) { + def snaps = iscsiClientSnapshots.getOrDefault(clientId, Collections.emptySet()) + def lunList = snaps.collect { snapId -> [id: snapId, lun_id: snapId] } + return [ret_code: "0", message: "", total: lunList.size(), luns: lunList, snapshots: lunList] + } + + // Handle gateways query: GET /block/iscsi/clients/{id}/gateways + if (uri.contains("/gateways") && "GET".equalsIgnoreCase(req.getMethod())) { + return [ret_code: "0", message: "", total: 1, gateways: [ + [id: "test-iscsi-target-default", name: "iscsi-target-default", + iqn: "iqn.2022-07.com.expontech.wds:test-iscsi-target-default", + port: 3260, run_status: "normal", + create_time: System.currentTimeMillis(), update_time: System.currentTimeMillis()] + ]] + } + + // Handle client GET by ID: GET /block/iscsi/clients/{id} + if ("GET".equalsIgnoreCase(req.getMethod())) { + int snapNum = iscsiClientSnapshots.getOrDefault(clientId, Collections.emptySet()).size() + return [ret_code: "0", message: "", id: clientId, name: clientId, hosts: [], + run_status: "normal", snap_num: snapNum, vol_num: 0, iscsi_gw_count: 1, + create_time: System.currentTimeMillis(), update_time: System.currentTimeMillis()] + } + + return [ret_code: "0", message: "", total: 0, gateways: [], luns: [], snapshots: []] + } + + // Query USS gateways (QueryUssGatewayRequest): GET /api/v2/wds/uss + simulator("/api/v2/(sync/)?wds/uss") { HttpServletRequest req, HttpEntity e, EnvSpec spec -> + def nameParam = req.getParameter("name") + def ussName = nameParam ?: "vhost_localhost" + def ussId = "test-uss-" + ussName + return [ret_code: "0", message: "", total: 1, uss_gateways: [ + [id: ussId, name: ussName, type: "uss", status: "health", + tianshu_id: TIANSHU_ID, tianshu_name: TIANSHU_NAME, + manager_ip: "127.0.0.1", business_port: 4420, business_network: "127.0.0.1/8", + create_time: System.currentTimeMillis(), update_time: System.currentTimeMillis()] + ]] + } + + // Query vhost controllers (QueryVhostControllerRequest): GET /api/v2/block/vhost + simulator("/api/v2/(sync/)?block/vhost") { HttpServletRequest req, HttpEntity e, EnvSpec spec -> + def vhostName = req.getParameter("name") + if (vhostName != null && !vhostNameToId.containsKey(vhostName)) { + return [ret_code: "0", message: "", total: 0, vhosts: []] + } + vhostName = vhostName ?: "vhost-default" + def vhostId = vhostNameToId.getOrDefault(vhostName, "test-vhost-" + vhostName) + return [ret_code: "0", message: "", total: 1, vhosts: [ + [id: vhostId, name: vhostName, status: "health", + path: "/var/run/vhost/" + vhostName, + create_time: System.currentTimeMillis(), update_time: System.currentTimeMillis()] + ]] + } + + // Create vhost controller: POST /api/v2/sync/block/vhost + simulator("/api/v2/sync/block/vhost") { HttpEntity e -> + def body = JSONObjectUtil.toObject(e.body, LinkedHashMap.class) + def vhostId = Platform.getUuid() + def vhostName = body?.name ?: "vhost-" + vhostId + vhostNameToId.put(vhostName, vhostId) + return [ret_code: "0", message: "", id: vhostId, name: vhostName] + } + + // Vhost controller operations with id (including bind/unbind USS, DELETE) + simulator("/api/v2/(sync/)?block/vhost/[^/]+(/.*)?") { HttpServletRequest req, HttpEntity e, EnvSpec spec -> + def uri = req.getRequestURI() + + // Handle DELETE vhost controller + if ("DELETE".equalsIgnoreCase(req.getMethod())) { + def matcher = (uri =~ /\/block\/vhost\/([^\/]+)/) + def vhostId = matcher ? matcher[0][1] : null + if (vhostId != null) { + vhostNameToId.entrySet().removeIf { it.value == vhostId } + vhostBoundUss.remove(vhostId) + } + return [ret_code: "0", message: ""] + } + + if (uri.contains("bind_uss") && !uri.contains("vhost_binded_uss")) { + def body = JSONObjectUtil.toObject(e.body, LinkedHashMap.class) + def vhostId = body?.vhost_id + if (vhostId != null) { + if (uri.contains("unbind_uss")) { + vhostBoundUss.remove(vhostId) + } else { + vhostBoundUss.add(vhostId) + } + } + return [ret_code: "0", message: ""] + } + if (uri.contains("vhost_binded_uss")) { + def matcher = (uri =~ /\/block\/vhost\/([^\/]+)\/vhost_binded_uss/) + def vhostId = matcher ? matcher[0][1] : null + if (vhostId != null && vhostBoundUss.contains(vhostId)) { + return [ret_code: "0", message: "", uss: [ + [id: "test-uss-vhost_localhost", name: "vhost_localhost", type: "uss", status: "health", + tianshu_id: TIANSHU_ID, tianshu_name: TIANSHU_NAME, + manager_ip: "127.0.0.1", business_port: 4420, business_network: "127.0.0.1/8"] + ]] + } + return [ret_code: "0", message: "", uss: []] + } + return [ret_code: "0", message: "", uss: []] + } + + // Query NVMf targets: GET /api/v2/block/nvmf + simulator("/api/v2/(sync/)?block/nvmf") { + return [ret_code: "0", message: "", total: 0, nvmfs: []] + } + + // Create NVMf target: POST /api/v2/sync/block/nvmf + simulator("/api/v2/sync/block/nvmf") { + def nvmfId = Platform.getUuid() + return [ret_code: "0", message: "", id: nvmfId] + } + + // NVMf bind/unbind USS + simulator("/api/v2/sync/block/nvmf/(un)?bind_uss") { + return [ret_code: "0", message: ""] + } + + // NVMf target operations with id + simulator("/api/v2/(sync/)?block/nvmf/[^/]+(/.*)?") { HttpServletRequest req, HttpEntity e, EnvSpec spec -> + return [ret_code: "0", message: "", uss: []] + } + + // Query NVMf client groups + simulator("/api/v2/(sync/)?block/nvmf_client/?") { + return [ret_code: "0", message: "", total: 0, clients: []] + } + + // Create NVMf client group: POST /api/v2/sync/block/nvmf_client + simulator("/api/v2/sync/block/nvmf_client") { + def clientId = Platform.getUuid() + return [ret_code: "0", message: "", id: clientId] + } + + // NVMf client group operations with id + simulator("/api/v2/(sync/)?block/nvmf_client/[^/]+(/.*)?") { HttpServletRequest req, HttpEntity e, EnvSpec spec -> + return [ret_code: "0", message: ""] + } + + // Create volume: POST /api/v2/sync/block/volumes + simulator("/api/v2/sync/block/volumes") { HttpEntity e -> + def body = JSONObjectUtil.toObject(e.body, LinkedHashMap.class) + def volId = Platform.getUuid() + def volName = body?.name ?: "vol-" + volumeCounter.incrementAndGet() + long volSize = body?.volume_size ?: SizeUnit.GIGABYTE.toByte(1) + + volumes.put(volId, [ + id: volId, name: volName, volume_name: volName, pool_id: POOL_ID, pool_name: POOL_NAME, + volume_size: volSize, data_size: 0, is_delete: false, run_status: "normal", + wwn: "wwn-" + volId + ]) + + return [ret_code: "0", message: "", id: volId] + } + + // Query volumes (QueryVolumeRequest): GET /api/v2/block/volumes + simulator("/api/v2/block/volumes") { HttpServletRequest req, HttpEntity e, EnvSpec spec -> + def allVols = volumes.values().toList() + return [ret_code: "0", message: "", total: allVols.size(), volumes: allVols] + } + + // Get volume detail (GetVolumeRequest): GET /api/v2/block/volumes/{volId} + simulator("/api/v2/block/volumes/[^/]+") { HttpServletRequest req, HttpEntity e, EnvSpec spec -> + def uri = req.getRequestURI() + def volId = uri.substring(uri.lastIndexOf("/") + 1) + def vol = volumes.get(volId) + if (vol == null) { + // try lookup by stripping dashes (expon IDs may or may not have dashes) + vol = volumes.get(volId.replace("-", "")) + } + if (vol == null) { + vol = [id: volId, name: "unknown", volume_name: "unknown", pool_id: POOL_ID, pool_name: POOL_NAME, + volume_size: SizeUnit.GIGABYTE.toByte(1), data_size: 0, is_delete: false, run_status: "normal"] + } + return [ret_code: "0", message: "", volume_detail: vol] + } + + // Delete volume: DELETE /api/v2/sync/block/volumes/{volId} + simulator("/api/v2/sync/block/volumes/[^/]+") { HttpServletRequest req, HttpEntity e, EnvSpec spec -> + def uri = req.getRequestURI() + def segments = uri.split("/") + def volId = segments[segments.length - 1] + volumes.remove(volId) + return [ret_code: "0", message: ""] + } + + // Expand volume: PUT /api/v2/sync/block/volumes/{id}/expand + simulator("/api/v2/sync/block/volumes/[^/]+/expand") { HttpEntity e -> + return [ret_code: "0", message: ""] + } + + // Set volume QoS: PUT /api/v2/sync/block/volumes/{volId}/qos + simulator("/api/v2/sync/block/volumes/[^/]+/qos") { HttpEntity e -> + return [ret_code: "0", message: ""] + } + + // Get volume LUN detail: GET /api/v2/sync/block/volumes/{volId}/lun_detail + simulator("/api/v2/sync/block/volumes/[^/]+/lun_detail") { HttpServletRequest req, HttpEntity e, EnvSpec spec -> + return [ret_code: "0", message: "", lun_details: [[lun_id: 0, lun_name: "lun-0"]]] + } + + // Get volume bound path: GET /api/v2/sync/block/volumes/{volId}/bind_status + simulator("/api/v2/sync/block/volumes/[^/]+/bind_status") { HttpServletRequest req, HttpEntity e, EnvSpec spec -> + return [ret_code: "0", message: "", bind_paths: []] + } + + // Get volume bound iSCSI client groups: GET /api/v2/block/volumes/{volumeId}/clients + simulator("/api/v2/block/volumes/[^/]+/clients") { HttpServletRequest req, HttpEntity e, EnvSpec spec -> + return [ret_code: "0", message: "", clients: []] + } + + // Recovery volume snapshot: PUT /api/v2/sync/block/volumes/{volumeId}/recovery + simulator("/api/v2/sync/block/volumes/[^/]+/recovery") { HttpEntity e -> + return [ret_code: "0", message: ""] + } + + // Get volume task progress: GET /api/v2/sync/block/volumes/tasks/{taskId} + simulator("/api/v2/sync/block/volumes/tasks/.*") { + return [ret_code: "0", message: "", status: "SUCCESS", progress: 100] + } + + // Update volume name: PUT /api/v2/block/volumes/{id}/name + simulator("/api/v2/block/volumes/[^/]+/name") { HttpEntity e -> + return [ret_code: "0", message: ""] + } + + // Create snapshot: POST /api/v2/sync/block/snaps + simulator("/api/v2/sync/block/snaps") { HttpEntity e -> + def body = JSONObjectUtil.toObject(e.body, LinkedHashMap.class) + def snapId = Platform.getUuid() + def snapName = body?.name ?: "snap-" + snapshotCounter.incrementAndGet() + def volId = body?.volume_id ?: "" + + def vol = volumes.get(volId) + long snapSize = vol != null ? (long) vol.get("volume_size") : SizeUnit.GIGABYTE.toByte(1) + + snapshots.put(snapId, [ + id: snapId, name: snapName, snap_name: snapName, snap_size: snapSize, + data_size: 0, volume_id: volId, volume_name: vol?.get("name") ?: "", + pool_id: POOL_ID, pool_name: POOL_NAME, is_delete: false, + wwn: "wwn-snap-" + snapId + ]) + + return [ret_code: "0", message: "", id: snapId] + } + + // Query snapshots (QueryVolumeSnapshotRequest): GET /api/v2/block/snaps + simulator("/api/v2/block/snaps") { HttpServletRequest req, HttpEntity e, EnvSpec spec -> + def allSnaps = snapshots.values().toList() + return [ret_code: "0", message: "", total: allSnaps.size(), snaps: allSnaps, volumes: []] + } + + // Get snapshot detail: GET /api/v2/sync/block/snaps/{id} + simulator("/api/v2/(sync/)?block/snaps/[^/]+") { HttpServletRequest req, HttpEntity e, EnvSpec spec -> + def uri = req.getRequestURI() + def snapId = uri.substring(uri.lastIndexOf("/") + 1) + def snap = snapshots.get(snapId) + if (snap == null) { + snap = [id: snapId, name: "unknown", snap_name: "unknown", snap_size: SizeUnit.GIGABYTE.toByte(1), + data_size: 0, volume_id: "", pool_id: POOL_ID, pool_name: POOL_NAME, is_delete: false] + } + return [ret_code: "0", message: "", snap_detail: snap] + } + + // Delete snapshot: DELETE /api/v2/sync/block/snaps/{snapshotId} + simulator("/api/v2/sync/block/snaps/[^/]+") { HttpServletRequest req, HttpEntity e, EnvSpec spec -> + def uri = req.getRequestURI() + def snapId = uri.substring(uri.lastIndexOf("/") + 1) + snapshots.remove(snapId) + return [ret_code: "0", message: ""] + } + + // Clone volume from snapshot: POST /api/v2/sync/block/snaps/{snapshotId}/clone + simulator("/api/v2/sync/block/snaps/[^/]+/clone") { HttpEntity e -> + def body = JSONObjectUtil.toObject(e.body, LinkedHashMap.class) + def volId = Platform.getUuid() + def volName = body?.name ?: "clone-" + volumeCounter.incrementAndGet() + + volumes.put(volId, [ + id: volId, name: volName, volume_name: volName, pool_id: POOL_ID, pool_name: POOL_NAME, + volume_size: SizeUnit.GIGABYTE.toByte(1), data_size: 0, is_delete: false, run_status: "normal", + wwn: "wwn-" + volId + ]) + + return [ret_code: "0", message: "", id: volId] + } + + // Copy snapshot: PUT /api/v2/sync/block/snaps/{snapshotId}/copy_clone + simulator("/api/v2/sync/block/snaps/[^/]+/copy_clone") { HttpEntity e -> + return [ret_code: "0", message: "", task_id: Platform.getUuid()] + } + + // Update snapshot: PUT /api/v2/block/snaps/{id} + simulator("/api/v2/block/snaps/[^/]+") { HttpEntity e -> + return [ret_code: "0", message: ""] + } + + // Set trash expire time: PUT /api/v1/sys_config/trash_recycle + simulator("/api/v1/sys_config/trash_recycle") { + return [ret_code: "0", message: ""] + } + } + } + + static class XinfiniSimulators implements Simulator { + static final int POOL_ID = 1 + static final String POOL_NAME = "pool1" + static final int BS_POLICY_ID = 1 + static final String CLUSTER_UUID = "test-xinfini-cluster-uuid" + static final int BDC_ID = 1 + static final int ISCSI_GATEWAY_ID = 1 + static final long TOTAL_CAPACITY_KB = 2L * 1024 * 1024 * 1024 // 2TB in KB + static final long USED_CAPACITY_KB = 700L * 1024 * 1024 // ~0.7TB in KB + + static ConcurrentHashMap volumes = new ConcurrentHashMap<>() + static ConcurrentHashMap snapshots = new ConcurrentHashMap<>() + static ConcurrentHashMap bdcBdevs = new ConcurrentHashMap<>() + static ConcurrentHashMap iscsiClients = new ConcurrentHashMap<>() + static ConcurrentHashMap iscsiClientGroups = new ConcurrentHashMap<>() + static ConcurrentHashMap volumeClientGroupMappings = new ConcurrentHashMap<>() + + static AtomicInteger volumeCounter = new AtomicInteger(0) + static AtomicInteger snapshotCounter = new AtomicInteger(0) + static AtomicInteger bdcBdevCounter = new AtomicInteger(0) + static AtomicInteger iscsiClientCounter = new AtomicInteger(0) + static AtomicInteger iscsiClientGroupCounter = new AtomicInteger(0) + static AtomicInteger volumeClientGroupMappingCounter = new AtomicInteger(0) + + static void clear() { + volumes.clear() + snapshots.clear() + bdcBdevs.clear() + iscsiClients.clear() + iscsiClientGroups.clear() + volumeClientGroupMappings.clear() + volumeCounter.set(0) + snapshotCounter.set(0) + bdcBdevCounter.set(0) + iscsiClientCounter.set(0) + iscsiClientGroupCounter.set(0) + volumeClientGroupMappingCounter.set(0) + } + + static Map makeQueryResponse(List items) { + return [ + metadata: [pagination: [count: items.size(), total_count: items.size(), offset: 0, limit: 100]], + items: items + ] + } + + static Map makeItemResponse(Map item) { + return [ + metadata: [id: item.spec?.id, name: item.spec?.name, state: [state: "active"]], + spec: item.spec, + status: item.status + ] + } + + static Map makeDeleteResponse() { + return [:] + } + + static Map makeNotFoundResponse() { + throw new HttpError(404, "not found") + } + + static List filterItems(List items, String qParam) { + if (qParam == null || qParam.isEmpty()) { + return items + } + + // Strip outer parentheses pairs + String q = qParam.trim() + + // Handle compound AND filters: ((spec.field1:val1) AND (spec.field2:val2)) + if (q.contains(" AND ")) { + def parts = q.split(" AND ") + List result = items + for (String part : parts) { + String cleaned = part.replaceAll("[()]", "").trim() + result = applySimpleFilter(result, cleaned) + } + return result + } + + // Simple filter: spec.field:value or (val1 val2) list + String cleaned = q.replaceAll("^\\(+", "").replaceAll("\\)+\$", "").trim() + return applySimpleFilter(items, cleaned) + } + + static List applySimpleFilter(List items, String filter) { + // Match pattern: spec.field:value or spec.field:(val1 val2 ...) + def matcher = (filter =~ /spec\.(\w+):\(?([^)]+)\)?/) + if (!matcher.find()) { + return items + } + String field = matcher.group(1) + String valueStr = matcher.group(2).trim() + + // Check if it's a list of values: (val1 val2 val3) + if (valueStr.contains(" ")) { + def values = valueStr.split("\\s+").toList() + return items.findAll { item -> + String itemVal = item.spec?.get(field)?.toString() + return itemVal != null && values.contains(itemVal) + } + } + + return items.findAll { item -> + String itemVal = item.spec?.get(field)?.toString() + return itemVal == valueStr + } + } + + static int extractIdFromUri(String uri) { + // Extract numeric ID from URI like /afa/v1/bs-volumes/5 + def matcher = (uri =~ /\/(\d+)(:[a-z-]+)?$/) + if (matcher.find()) { + return Integer.parseInt(matcher.group(1)) + } + // Try extracting ID before :action like /afa/v1/bs-volumes/5:flatten + matcher = (uri =~ /\/(\d+):/) + if (matcher.find()) { + return Integer.parseInt(matcher.group(1)) + } + // Try extracting ID before /:action like /afa/v1/bs-volumes/5/:add-client-group-mappings + matcher = (uri =~ /\/(\d+)\/:[a-z-]+/) + if (matcher.find()) { + return Integer.parseInt(matcher.group(1)) + } + return -1 + } + + @Override + void registerSimulators(EnvSpec espec) { + def simulator = { arg1, arg2 -> + espec.simulator(arg1, arg2) + } + + // ========== SDDC Category ========== + + // 1. GET /sddc/v1/cluster - QueryClusterRequest + simulator("/sddc/v1/cluster") { HttpServletRequest req, HttpEntity e, EnvSpec spec -> + return makeQueryResponse([ + [ + metadata: [id: 1, name: "xinfini-cluster", state: [state: "active"]], + spec: [id: 1, name: "xinfini-cluster", uuid: CLUSTER_UUID], + status: [id: 1] + ] + ]) + } + + // 2. GET /sddc/v1/samples/query - QueryMetricRequest + simulator("/sddc/v1/samples/query") { HttpServletRequest req, HttpEntity e, EnvSpec spec -> + def metricParam = req.getParameter("metric") + long value + if (metricParam != null && metricParam.contains("data_kbytes")) { + // Used capacity + value = USED_CAPACITY_KB + } else { + // Total capacity (actual_kbytes) + value = TOTAL_CAPACITY_KB + } + return [data: [result_type: "vector", result: [[value: value]]]] + } + + // ========== AFA Category: Pool & Node ========== + + // 3. GET /afa/v1/pools - QueryPoolRequest + simulator("/afa/v1/pools") { HttpServletRequest req, HttpEntity e, EnvSpec spec -> + def now = java.time.OffsetDateTime.now().toString() + return makeQueryResponse([ + [ + metadata: [id: POOL_ID, name: POOL_NAME, state: [state: "active"]], + spec: [id: POOL_ID, name: POOL_NAME, default_bs_policy_id: BS_POLICY_ID, created_at: now, updated_at: now], + status: [id: POOL_ID] + ] + ]) + } + + // 4. GET /afa/v1/pools/{id} - GetPoolRequest + simulator("/afa/v1/pools/\\d+") { HttpServletRequest req, HttpEntity e, EnvSpec spec -> + def now = java.time.OffsetDateTime.now().toString() + return [ + metadata: [id: POOL_ID, name: POOL_NAME, state: [state: "active"]], + spec: [id: POOL_ID, name: POOL_NAME, default_bs_policy_id: BS_POLICY_ID, created_at: now, updated_at: now], + status: [id: POOL_ID] + ] + } + + // 5. GET /afa/v1/bs-policies/{id} - GetBsPolicyRequest + simulator("/afa/v1/bs-policies/\\d+") { HttpServletRequest req, HttpEntity e, EnvSpec spec -> + return [ + metadata: [id: BS_POLICY_ID, name: "default-policy", state: [state: "active"]], + spec: [id: BS_POLICY_ID, name: "default-policy", data_replica_type: "replica", data_replica_num: 3], + status: [id: BS_POLICY_ID] + ] + } + + // 6. GET /afa/v1/nodes - QueryNodeRequest + simulator("/afa/v1/nodes") { HttpServletRequest req, HttpEntity e, EnvSpec spec -> + return makeQueryResponse([ + [ + metadata: [id: 1, name: "node-1", state: [state: "active"]], + spec: [id: 1, name: "node-1", ip: "127.0.0.1", port: 80, admin_ip: "127.0.0.1", role_afa_admin: true, role_afa_server: true, storage_public_ip: "127.0.0.1", storage_private_ip: "127.0.0.1"], + status: [id: 1, run_state: "Active"] + ] + ]) + } + + // 7. GET /afa/v1/nodes/{id} - GetNodeRequest + simulator("/afa/v1/nodes/\\d+") { HttpServletRequest req, HttpEntity e, EnvSpec spec -> + return [ + metadata: [id: 1, name: "node-1", state: [state: "active"]], + spec: [id: 1, name: "node-1", ip: "127.0.0.1", port: 80, admin_ip: "127.0.0.1", role_afa_admin: true, role_afa_server: true, storage_public_ip: "127.0.0.1", storage_private_ip: "127.0.0.1"], + status: [id: 1, run_state: "Active"] + ] + } + + // ========== AFA Category: Volume ========== + + // 8 & 9. /afa/v1/bs-volumes - POST create, GET query + simulator("/afa/v1/bs-volumes") { HttpServletRequest req, HttpEntity e, EnvSpec spec -> + if (req.getMethod() == "POST") { + // 8. CreateVolumeRequest + def body = JSONObjectUtil.toObject(e.body, LinkedHashMap.class) + def specData = body?.spec ?: body + int volId = volumeCounter.incrementAndGet() + String volName = specData?.name ?: "vol-${volId}" + int poolId = specData?.pool_id ?: POOL_ID + long sizeMb = specData?.size_mb ?: 1024 + + def volSpec = [ + id: volId, name: volName, pool_id: poolId, size_mb: sizeMb, + bs_policy_id: BS_POLICY_ID, serial: "serial-${volId}".toString(), + loaded: false, flattened: true, max_total_iops: 0, max_total_bw_bps: 0, + creator: specData?.creator ?: "zstack", uuid: Platform.getUuid(), etag: Platform.getUuid() + ] + def volStatus = [ + id: volId, size_mb: sizeMb, allocated_size_byte: 0, + loaded: false, spring_id: 0, protocol: "", mapping_num: 0 + ] + def volItem = [spec: volSpec, status: volStatus] + volumes.put(volId, volItem) + + return makeItemResponse(volItem) + } else { + // 9. QueryVolumeRequest + def qParam = req.getParameter("q") + def allItems = volumes.values().collect { vol -> + [ + metadata: [id: vol.spec.id, name: vol.spec.name, state: [state: "active"]], + spec: vol.spec, + status: vol.status + ] + } + def filtered = filterItems(allItems, qParam) + return makeQueryResponse(filtered) + } + } + + // 10, 11, 12. /afa/v1/bs-volumes/{id} - GET get, PATCH update, DELETE delete + simulator("/afa/v1/bs-volumes/\\d+") { HttpServletRequest req, HttpEntity e, EnvSpec spec -> + int volId = extractIdFromUri(req.getRequestURI()) + if (req.getMethod() == "DELETE") { + // 12. DeleteVolumeRequest + volumes.remove(volId) + return makeDeleteResponse() + } else if (req.getMethod() == "PATCH") { + // 11. UpdateVolumeRequest + def vol = volumes.get(volId) + if (vol != null) { + def body = JSONObjectUtil.toObject(e.body, LinkedHashMap.class) + def specData = body?.spec ?: body + if (specData?.size_mb) { + vol.spec.size_mb = specData.size_mb + vol.status.size_mb = specData.size_mb + } + if (specData?.max_total_iops != null) { + vol.spec.max_total_iops = specData.max_total_iops + } + if (specData?.max_total_bw_bps != null) { + vol.spec.max_total_bw_bps = specData.max_total_bw_bps + } + } + if (vol == null) { + return makeNotFoundResponse() + } + return makeItemResponse(vol) + } else { + // 10. GetVolumeRequest + def vol = volumes.get(volId) + if (vol == null) { + return makeNotFoundResponse() + } + return makeItemResponse(vol) + } + } + + // 13. POST /afa/v1/bs-volumes/:clone - CloneVolumeRequest + simulator("/afa/v1/bs-volumes/:clone") { HttpServletRequest req, HttpEntity e, EnvSpec spec -> + def body = JSONObjectUtil.toObject(e.body, LinkedHashMap.class) + def specData = body?.spec ?: body + int volId = volumeCounter.incrementAndGet() + String volName = specData?.name ?: "clone-${volId}" + int bsSnapId = specData?.bs_snap_id ?: 0 + + // Get size from source snapshot if available + long sizeMb = 1024 + def srcSnap = snapshots.get(bsSnapId) + if (srcSnap != null) { + sizeMb = srcSnap.spec.size_mb ?: 1024 + } + + def volSpec = [ + id: volId, name: volName, pool_id: POOL_ID, size_mb: sizeMb, + bs_policy_id: BS_POLICY_ID, bs_snap_id: bsSnapId, + serial: "serial-${volId}".toString(), loaded: false, flattened: false, + max_total_iops: 0, max_total_bw_bps: 0, + creator: specData?.creator ?: "zstack", uuid: Platform.getUuid(), etag: Platform.getUuid() + ] + def volStatus = [ + id: volId, size_mb: sizeMb, allocated_size_byte: 0, + loaded: false, spring_id: 0, protocol: "", mapping_num: 0 + ] + def volItem = [spec: volSpec, status: volStatus] + volumes.put(volId, volItem) + + return makeItemResponse(volItem) + } + + // 14. POST /afa/v1/bs-volumes/{id}/:flatten - FlattenVolumeRequest + simulator("/afa/v1/bs-volumes/\\d+/:flatten") { HttpServletRequest req, HttpEntity e, EnvSpec spec -> + int volId = extractIdFromUri(req.getRequestURI()) + def vol = volumes.get(volId) + if (vol != null) { + vol.spec.flattened = true + } + if (vol == null) { + return makeNotFoundResponse() + } + return makeItemResponse(vol) + } + + // 15. POST /afa/v1/bs-volumes/{id}/:rollback - RollbackSnapshotRequest + simulator("/afa/v1/bs-volumes/\\d+/:rollback") { HttpServletRequest req, HttpEntity e, EnvSpec spec -> + int volId = extractIdFromUri(req.getRequestURI()) + def vol = volumes.get(volId) + if (vol == null) { + return makeNotFoundResponse() + } + return makeItemResponse(vol) + } + + // ========== AFA Category: Snapshot ========== + + // 16 & 17. /afa/v1/bs-snaps - POST create, GET query + simulator("/afa/v1/bs-snaps") { HttpServletRequest req, HttpEntity e, EnvSpec spec -> + if (req.getMethod() == "POST") { + // 16. CreateVolumeSnapshotRequest + def body = JSONObjectUtil.toObject(e.body, LinkedHashMap.class) + def specData = body?.spec ?: body + int snapId = snapshotCounter.incrementAndGet() + String snapName = specData?.name ?: "snap-${snapId}" + int bsVolumeId = specData?.bs_volume_id ?: 0 + + long sizeMb = 1024 + def srcVol = volumes.get(bsVolumeId) + if (srcVol != null) { + sizeMb = srcVol.spec.size_mb ?: 1024 + } + + def snapSpec = [ + id: snapId, name: snapName, pool_id: POOL_ID, + bs_volume_id: bsVolumeId, bs_policy_id: BS_POLICY_ID, + size_mb: sizeMb, creator: specData?.creator ?: "zstack", + uuid: Platform.getUuid() + ] + def snapStatus = [id: snapId, size_mb: sizeMb] + def snapItem = [spec: snapSpec, status: snapStatus] + snapshots.put(snapId, snapItem) + + return makeItemResponse(snapItem) + } else { + // 17. QueryVolumeSnapshotRequest + def qParam = req.getParameter("q") + def allItems = snapshots.values().collect { snap -> + [ + metadata: [id: snap.spec.id, name: snap.spec.name, state: [state: "active"]], + spec: snap.spec, + status: snap.status + ] + } + def filtered = filterItems(allItems, qParam) + return makeQueryResponse(filtered) + } + } + + // 18 & 19. /afa/v1/bs-snaps/{id} - GET get, DELETE delete + simulator("/afa/v1/bs-snaps/\\d+") { HttpServletRequest req, HttpEntity e, EnvSpec spec -> + int snapId = extractIdFromUri(req.getRequestURI()) + if (req.getMethod() == "DELETE") { + // 19. DeleteVolumeSnapshotRequest + snapshots.remove(snapId) + return makeDeleteResponse() + } else { + // 18. GetVolumeSnapshotRequest + def snap = snapshots.get(snapId) + if (snap == null) { + return makeNotFoundResponse() + } + return makeItemResponse(snap) + } + } + + // ========== AFA Category: BDC / BdcBdev (Vhost) ========== + + // 20. GET /afa/v1/bdcs - QueryBdcRequest + simulator("/afa/v1/bdcs") { HttpServletRequest req, HttpEntity e, EnvSpec spec -> + // In UNIT_TEST_ON mode, queryBdcByIp uses sortBy=spec.id:desc instead of q=spec.ip:xxx + // Return all BDCs sorted by id desc + def bdcItems = [ + [ + metadata: [id: BDC_ID, name: "bdc-1", state: [state: "active"]], + spec: [id: BDC_ID, name: "bdc-1", ip: "127.0.0.1", port: 9500], + status: [id: BDC_ID, run_state: "Active", installed: true, hostname: "localhost", version: "1.0.0"] + ] + ] + return makeQueryResponse(bdcItems) + } + + // 21. GET /afa/v1/bdcs/{id} - GetBdcRequest + simulator("/afa/v1/bdcs/\\d+") { HttpServletRequest req, HttpEntity e, EnvSpec spec -> + return [ + metadata: [id: BDC_ID, name: "bdc-1", state: [state: "active"]], + spec: [id: BDC_ID, name: "bdc-1", ip: "127.0.0.1", port: 9500], + status: [id: BDC_ID, run_state: "Active", installed: true, hostname: "localhost", version: "1.0.0"] + ] + } + + // 22 & 23. /afa/v1/bdc-bdevs - POST create, GET query + simulator("/afa/v1/bdc-bdevs") { HttpServletRequest req, HttpEntity e, EnvSpec spec -> + if (req.getMethod() == "POST") { + // 22. CreateBdcBdevRequest + def body = JSONObjectUtil.toObject(e.body, LinkedHashMap.class) + def specData = body?.spec ?: body + int bdevId = bdcBdevCounter.incrementAndGet() + int bdcId = specData?.bdc_id ?: BDC_ID + int bsVolumeId = specData?.bs_volume_id ?: 0 + String bdevName = specData?.name ?: "volume-${bdevId}" + int queueNum = specData?.queue_num ?: 1 + String socketPath = "/var/run/bdc-${CLUSTER_UUID}/${bdevName}" + + def bdevSpec = [ + id: bdevId, name: bdevName, bdc_id: bdcId, node_ip: "127.0.0.1", + bs_volume_id: bsVolumeId, socket_path: socketPath, queue_num: queueNum, + bs_volume_name: bdevName, bs_volume_uuid: Platform.getUuid(), numa_node_ids: [] + ] + def bdevStatus = [id: bdevId] + def bdevItem = [spec: bdevSpec, status: bdevStatus] + bdcBdevs.put(bdevId, bdevItem) + + return makeItemResponse(bdevItem) + } else { + // 23. QueryBdcBdevRequest + def qParam = req.getParameter("q") + def allItems = bdcBdevs.values().collect { bdev -> + [ + metadata: [id: bdev.spec.id, name: bdev.spec.name, state: [state: "active"]], + spec: bdev.spec, + status: bdev.status + ] + } + def filtered = filterItems(allItems, qParam) + return makeQueryResponse(filtered) + } + } + + // 24 & 25. /afa/v1/bdc-bdevs/{id} - GET get, DELETE delete + simulator("/afa/v1/bdc-bdevs/\\d+") { HttpServletRequest req, HttpEntity e, EnvSpec spec -> + int bdevId = extractIdFromUri(req.getRequestURI()) + if (req.getMethod() == "DELETE") { + // 25. DeleteBdcBdevRequest + bdcBdevs.remove(bdevId) + return makeDeleteResponse() + } else { + // 24. GetBdcBdevRequest + def bdev = bdcBdevs.get(bdevId) + if (bdev == null) { + return makeNotFoundResponse() + } + return makeItemResponse(bdev) + } + } + + // ========== AFA Category: iSCSI ========== + + // 26. GET /afa/v1/iscsi-gateways - QueryIscsiGatewayRequest + simulator("/afa/v1/iscsi-gateways") { HttpServletRequest req, HttpEntity e, EnvSpec spec -> + def gwItems = [ + [ + metadata: [id: ISCSI_GATEWAY_ID, name: "iscsi-gw-1", state: [state: "active"]], + spec: [id: ISCSI_GATEWAY_ID, name: "iscsi-gw-1", node_id: 1, ips: ["127.0.0.1"], port: 3260], + status: [id: ISCSI_GATEWAY_ID, node_state: "ACTIVE"] + ] + ] + return makeQueryResponse(gwItems) + } + + // 27 & 29. /afa/v1/iscsi-clients - GET query, POST create + simulator("/afa/v1/iscsi-clients") { HttpServletRequest req, HttpEntity e, EnvSpec spec -> + if (req.getMethod() == "POST") { + // 29. CreateIscsiClientRequest + def body = JSONObjectUtil.toObject(e.body, LinkedHashMap.class) + def specData = body?.spec ?: body + int clientId = iscsiClientCounter.incrementAndGet() + String clientName = specData?.name ?: "iscsi-client-${clientId}" + String code = specData?.code ?: "iqn.2000-01.com.example:client-${clientId}" + Integer clientGroupId = specData?.iscsi_client_group_id + + def clientSpec = [ + id: clientId, name: clientName, code: code, + iscsi_client_group_id: clientGroupId + ] + def clientStatus = [id: clientId, target_iqns: []] + def clientItem = [spec: clientSpec, status: clientStatus] + iscsiClients.put(clientId, clientItem) + + return makeItemResponse(clientItem) + } else { + // 27. QueryIscsiClientRequest + def qParam = req.getParameter("q") + def allItems = iscsiClients.values().collect { client -> + [ + metadata: [id: client.spec.id, name: client.spec.name, state: [state: "active"]], + spec: client.spec, + status: client.status + ] + } + def filtered = filterItems(allItems, qParam) + return makeQueryResponse(filtered) + } + } + + // 28 & 30. /afa/v1/iscsi-clients/{id} - GET get, DELETE delete + simulator("/afa/v1/iscsi-clients/\\d+") { HttpServletRequest req, HttpEntity e, EnvSpec spec -> + int clientId = extractIdFromUri(req.getRequestURI()) + if (req.getMethod() == "DELETE") { + // 30. DeleteIscsiClientRequest + iscsiClients.remove(clientId) + return makeDeleteResponse() + } else { + // 28. GetIscsiClientRequest + def client = iscsiClients.get(clientId) + if (client == null) { + return makeNotFoundResponse() + } + return makeItemResponse(client) + } + } + + // 31 & 33. /afa/v1/iscsi-client-groups - GET query, POST create + simulator("/afa/v1/iscsi-client-groups") { HttpServletRequest req, HttpEntity e, EnvSpec spec -> + if (req.getMethod() == "POST") { + // 33. CreateIscsiClientGroupRequest + def body = JSONObjectUtil.toObject(e.body, LinkedHashMap.class) + def specData = body?.spec ?: body + int groupId = iscsiClientGroupCounter.incrementAndGet() + String groupName = specData?.name ?: "iscsi-client-group-${groupId}" + + // Also create iSCSI clients for each client code (IQN) + def clientCodes = specData?.iscsi_client_codes ?: [] + String targetIqn = "iqn.2022-07.com.xinfini:target-${groupId}".toString() + clientCodes.each { code -> + int cId = iscsiClientCounter.incrementAndGet() + def cSpec = [id: cId, name: "iscsi-client-${cId}".toString(), code: code, iscsi_client_group_id: groupId] + def cStatus = [id: cId, target_iqns: [targetIqn]] + def cItem = [spec: cSpec, status: cStatus] + iscsiClients.put(cId, cItem) + } + + def groupSpec = [id: groupId, name: groupName] + def groupStatus = [id: groupId] + def groupItem = [spec: groupSpec, status: groupStatus] + iscsiClientGroups.put(groupId, groupItem) + + return makeItemResponse(groupItem) + } else { + // 31. QueryIscsiClientGroupRequest + def qParam = req.getParameter("q") + def allItems = iscsiClientGroups.values().collect { group -> + [ + metadata: [id: group.spec.id, name: group.spec.name, state: [state: "active"]], + spec: group.spec, + status: group.status + ] + } + def filtered = filterItems(allItems, qParam) + return makeQueryResponse(filtered) + } + } + + // 32. GET /afa/v1/iscsi-client-groups/{id} - GetIscsiClientGroupRequest + simulator("/afa/v1/iscsi-client-groups/\\d+") { HttpServletRequest req, HttpEntity e, EnvSpec spec -> + int groupId = extractIdFromUri(req.getRequestURI()) + def group = iscsiClientGroups.get(groupId) + if (group == null) { + return makeNotFoundResponse() + } + return makeItemResponse(group) + } + + // 34. GET /afa/v1/iscsi-gateway-client-group-mappings - QueryIscsiGatewayClientGroupMappingRequest + simulator("/afa/v1/iscsi-gateway-client-group-mappings") { HttpServletRequest req, HttpEntity e, EnvSpec spec -> + // Return mappings based on existing client groups + def mappingItems = iscsiClientGroups.values().collect { group -> + [ + metadata: [id: group.spec.id, name: "gw-group-mapping-${group.spec.id}".toString(), state: [state: "active"]], + spec: [id: group.spec.id, iscsi_gateway_id: ISCSI_GATEWAY_ID, iscsi_client_group_id: group.spec.id], + status: [id: group.spec.id] + ] + } + return makeQueryResponse(mappingItems) + } + + // 35 & 37. /afa/v1/bs-volume-client-group-mappings - GET query (35), also handle DELETE for {id} (37) + simulator("/afa/v1/bs-volume-client-group-mappings") { HttpServletRequest req, HttpEntity e, EnvSpec spec -> + // 35. QueryVolumeClientGroupMappingRequest + def qParam = req.getParameter("q") + def allItems = volumeClientGroupMappings.values().collect { mapping -> + [ + metadata: [id: mapping.spec.id, name: "vol-group-mapping-${mapping.spec.id}".toString(), state: [state: "active"]], + spec: mapping.spec, + status: mapping.status + ] + } + def filtered = filterItems(allItems, qParam) + return makeQueryResponse(filtered) + } + + // 36 & 37. /afa/v1/bs-volume-client-group-mappings/{id} - GET get (36), DELETE delete (37) + simulator("/afa/v1/bs-volume-client-group-mappings/\\d+") { HttpServletRequest req, HttpEntity e, EnvSpec spec -> + int mappingId = extractIdFromUri(req.getRequestURI()) + if (req.getMethod() == "DELETE") { + // 37. DeleteVolumeClientGroupMappingRequest + volumeClientGroupMappings.remove(mappingId) + return makeDeleteResponse() + } else { + // 36. GetVolumeClientGroupMappingRequest + def mapping = volumeClientGroupMappings.get(mappingId) + if (mapping == null) { + return makeNotFoundResponse() + } + return makeItemResponse(mapping) + } + } + + // 38. POST /afa/v1/bs-volumes/{id}/:add-client-group-mappings - AddVolumeClientGroupMappingRequest + simulator("/afa/v1/bs-volumes/\\d+/:add-client-group-mappings") { HttpServletRequest req, HttpEntity e, EnvSpec spec -> + int volId = extractIdFromUri(req.getRequestURI()) + def body = JSONObjectUtil.toObject(e.body, LinkedHashMap.class) + def specData = body?.spec ?: body + def groupIds = specData?.iscsi_client_group_ids ?: [] + + def createdMappings = [] + for (gid in groupIds) { + int mappingId = volumeClientGroupMappingCounter.incrementAndGet() + int groupId = gid instanceof Integer ? gid : Integer.parseInt(gid.toString()) + def mappingSpec = [ + id: mappingId, bs_volume_id: volId, iscsi_client_group_id: groupId, lun_id: mappingId + ] + def mappingStatus = [id: mappingId] + def mappingItem = [spec: mappingSpec, status: mappingStatus] + volumeClientGroupMappings.put(mappingId, mappingItem) + createdMappings.add([ + metadata: [id: mappingId, name: "vol-group-mapping-${mappingId}".toString(), state: [state: "active"]], + spec: mappingSpec, + status: mappingStatus + ]) + } + + return makeQueryResponse(createdMappings) + } + + // 39. GET /afa/v1/bs-volume-client-mappings - QueryVolumeClientMappingRequest + simulator("/afa/v1/bs-volume-client-mappings") { HttpServletRequest req, HttpEntity e, EnvSpec spec -> + // Build volume-client mappings from volume-client-group mappings and iscsi clients + def mappingItems = [] + int mappingIdSeq = 0 + volumeClientGroupMappings.values().each { vcgMapping -> + int volId = vcgMapping.spec.bs_volume_id + int groupId = vcgMapping.spec.iscsi_client_group_id + // Find clients in this group + iscsiClients.values().each { client -> + if (client.spec.iscsi_client_group_id == groupId) { + mappingIdSeq++ + mappingItems.add([ + metadata: [id: mappingIdSeq, name: "vol-client-mapping-${mappingIdSeq}".toString(), state: [state: "active"]], + spec: [ + id: mappingIdSeq, bs_volume_id: volId, + iscsi_client_id: client.spec.id, + iscsi_client_group_id: groupId, + protocol: "iSCSI", lun_id: vcgMapping.spec.lun_id + ], + status: [id: mappingIdSeq] + ]) + } + } + } + + def qParam = req.getParameter("q") + def filtered = filterItems(mappingItems, qParam) + return makeQueryResponse(filtered) + } + } + } + @Override SpecID create(String uuid, String sessionId) { inventory = addExternalPrimaryStorage { diff --git a/testlib/src/main/java/org/zstack/testlib/SpringSpec.groovy b/testlib/src/main/java/org/zstack/testlib/SpringSpec.groovy index 6346b1cc68d..a8e6812c992 100755 --- a/testlib/src/main/java/org/zstack/testlib/SpringSpec.groovy +++ b/testlib/src/main/java/org/zstack/testlib/SpringSpec.groovy @@ -105,6 +105,10 @@ class SpringSpec { include("iscsi.xml") } + void xinfini() { + include("xinfini.xml") + } + void zbs() { include("zbs.xml") include("cbd.xml") diff --git a/testlib/src/main/java/org/zstack/testlib/Test.groovy b/testlib/src/main/java/org/zstack/testlib/Test.groovy index 1dcb02c54e9..40f46defad1 100755 --- a/testlib/src/main/java/org/zstack/testlib/Test.groovy +++ b/testlib/src/main/java/org/zstack/testlib/Test.groovy @@ -120,6 +120,8 @@ abstract class Test extends ApiHelper implements Retry { lb() nfsPrimaryStorage() externalPrimaryStorage() + expon() + xinfini() zbs() eip() portForwarding() @@ -999,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/utils/src/main/java/org/zstack/utils/clouderrorcode/CloudOperationsErrorCode.java b/utils/src/main/java/org/zstack/utils/clouderrorcode/CloudOperationsErrorCode.java index a0f09d4f1e9..ac2eeb7fd8f 100644 --- a/utils/src/main/java/org/zstack/utils/clouderrorcode/CloudOperationsErrorCode.java +++ b/utils/src/main/java/org/zstack/utils/clouderrorcode/CloudOperationsErrorCode.java @@ -970,6 +970,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 +1022,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"; @@ -1654,6 +1662,10 @@ public class CloudOperationsErrorCode { public static final String ORG_ZSTACK_LDAP_10019 = "ORG_ZSTACK_LDAP_10019"; + public static final String ORG_ZSTACK_LDAP_10020 = "ORG_ZSTACK_LDAP_10020"; + + public static final String ORG_ZSTACK_LDAP_10021 = "ORG_ZSTACK_LDAP_10021"; + public static final String ORG_ZSTACK_IMAGE_10000 = "ORG_ZSTACK_IMAGE_10000"; public static final String ORG_ZSTACK_IMAGE_10001 = "ORG_ZSTACK_IMAGE_10001"; @@ -3230,6 +3242,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"; @@ -5478,6 +5492,14 @@ 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_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"; @@ -6274,6 +6296,8 @@ public class CloudOperationsErrorCode { public static final String ORG_ZSTACK_STORAGE_BACKUP_10133 = "ORG_ZSTACK_STORAGE_BACKUP_10133"; + public static final String ORG_ZSTACK_STORAGE_BACKUP_CANCEL_TIMEOUT = "ORG_ZSTACK_STORAGE_BACKUP_CANCEL_TIMEOUT"; + public static final String ORG_ZSTACK_COMPUTE_10000 = "ORG_ZSTACK_COMPUTE_10000"; public static final String ORG_ZSTACK_COMPUTE_10001 = "ORG_ZSTACK_COMPUTE_10001"; @@ -6500,6 +6524,8 @@ public class CloudOperationsErrorCode { public static final String ORG_ZSTACK_STORAGE_ADDON_PRIMARY_10014 = "ORG_ZSTACK_STORAGE_ADDON_PRIMARY_10014"; + public static final String ORG_ZSTACK_STORAGE_ADDON_PRIMARY_10015 = "ORG_ZSTACK_STORAGE_ADDON_PRIMARY_10015"; + public static final String ORG_ZSTACK_STORAGE_ADDON_PRIMARY_10040 = "ORG_ZSTACK_STORAGE_ADDON_PRIMARY_10040"; public static final String ORG_ZSTACK_NETWORK_HOSTNETWORKINTERFACE_LLDP_10000 = "ORG_ZSTACK_NETWORK_HOSTNETWORKINTERFACE_LLDP_10000"; @@ -7692,6 +7718,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"; @@ -9828,6 +9860,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"; @@ -10380,6 +10432,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"; @@ -10652,6 +10705,8 @@ public class CloudOperationsErrorCode { public static final String ORG_ZSTACK_PREMIUM_EXTERNALSERVICE_PROMETHEUS_10013 = "ORG_ZSTACK_PREMIUM_EXTERNALSERVICE_PROMETHEUS_10013"; + public static final String ORG_ZSTACK_PREMIUM_EXTERNALSERVICE_PROMETHEUS_10014 = "ORG_ZSTACK_PREMIUM_EXTERNALSERVICE_PROMETHEUS_10014"; + public static final String ORG_ZSTACK_AI_CONTAINER_10000 = "ORG_ZSTACK_AI_CONTAINER_10000"; public static final String ORG_ZSTACK_AI_CONTAINER_10001 = "ORG_ZSTACK_AI_CONTAINER_10001"; @@ -11586,6 +11641,8 @@ public class CloudOperationsErrorCode { public static final String ORG_ZSTACK_PCIDEVICE_10076 = "ORG_ZSTACK_PCIDEVICE_10076"; + public static final String ORG_ZSTACK_PCIDEVICE_10077 = "ORG_ZSTACK_PCIDEVICE_10077"; + public static final String ORG_ZSTACK_CAS_DRIVER_DONGHAI_10000 = "ORG_ZSTACK_CAS_DRIVER_DONGHAI_10000"; public static final String ORG_ZSTACK_CAS_DRIVER_DONGHAI_10001 = "ORG_ZSTACK_CAS_DRIVER_DONGHAI_10001"; @@ -13600,6 +13657,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"; @@ -13730,6 +13797,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"; @@ -14802,6 +14871,8 @@ public class CloudOperationsErrorCode { public static final String ORG_ZSTACK_AI_10133 = "ORG_ZSTACK_AI_10133"; + public static final String ORG_ZSTACK_AI_10134 = "ORG_ZSTACK_AI_10134"; + 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"; @@ -15118,6 +15189,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"; 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; } } diff --git a/utils/src/main/java/org/zstack/utils/string/StringSimilarity.java b/utils/src/main/java/org/zstack/utils/string/StringSimilarity.java index ee5257d1640..be5d1a5442f 100644 --- a/utils/src/main/java/org/zstack/utils/string/StringSimilarity.java +++ b/utils/src/main/java/org/zstack/utils/string/StringSimilarity.java @@ -32,6 +32,9 @@ public class StringSimilarity { private static final int mapLength = 1500; public static int maxElaborationRegex = 8192; + // slow elaboration warn threshold in ms; 0 = disabled (default) + // enable via GlobalProperty or test setup: StringSimilarity.slowElaborationThresholdMs = 200; + public static long slowElaborationThresholdMs = 0; // matched errors private static final Map errors = new LinkedHashMap(mapLength, 0.9f, true) { @@ -280,13 +283,29 @@ private static boolean isRedundant(String sub) { } private static void logSearchSpend(String sub, long start, boolean found) { - logger.debug(String.format("[%s] spend %s ms to search elaboration \"%s\"", found, System.currentTimeMillis() - start, - sub.length() > 50 ? sub.substring(0 , 50) + "..." : sub)); + long cost = System.currentTimeMillis() - start; + if (slowElaborationThresholdMs > 0 && cost > slowElaborationThresholdMs) { + logger.warn(String.format("[SLOW-ELABORATION] cost %d ms, found=%s, input_len=%d, input=%s", + cost, found, sub.length(), + sub.length() > 100 ? sub.substring(0, 100) + "..." : sub)); + } else { + logger.debug(String.format("[%s] spend %s ms to search elaboration \"%s\"", found, cost, + sub.length() > 50 ? sub.substring(0, 50) + "..." : sub)); + } } /** * find the most similar error code elaboration for the given error message. * + * The method uses a two-phase strategy to avoid performance degradation + * when format args produce very long strings (e.g., serialized error chains + * or HTML response bodies): + * + * Phase 1: Try regex matching with the formatted string (length-guarded). + * Phase 2: If Phase 1 misses (or formatted string too long), fallback + * to the raw fmt template for regex matching. + * Phase 3: Distance matching always uses the raw fmt template. + * * @param sub error message or error message fmt * @param args arguments * @return the most similar error code elaboration @@ -297,7 +316,10 @@ public static ErrorCodeElaboration findSimilar(String sub, Object...args) { } long start = System.currentTimeMillis(); - ErrorCodeElaboration err = errors.get(sub); + ErrorCodeElaboration err; + synchronized (errors) { + err = errors.get(sub); + } if (err != null && verifyElaboration(err, sub, args)) { logSearchSpend(sub, start, true); return err; @@ -308,26 +330,50 @@ public static ErrorCodeElaboration findSimilar(String sub, Object...args) { // same cause will happen // invalid cache, generate elaboration again if (err != null) { - errors.remove(sub); + synchronized (errors) { + errors.remove(sub); + } } - if (args != null && missed.get(String.format(sub, args)) != null) { - logSearchSpend(sub, start, false); - return null; - } else if (missed.get(sub) != null) { - logSearchSpend(sub, start, false); - return null; + // check missed cache for both fmt template and formatted string + synchronized (missed) { + if (missed.get(sub) != null) { + logSearchSpend(sub, start, false); + return null; + } + } + if (args != null) { + try { + String formatted = String.format(sub, args); + synchronized (missed) { + if (missed.get(formatted) != null) { + logSearchSpend(sub, start, false); + return null; + } + } + } catch (Exception e) { + logger.trace(String.format("failed to format elaboration key: %s", e.getMessage())); + } } - try { - logger.trace(String.format("start to search elaboration for: %s", String.format(sub, args))); - err = findMostSimilarRegex(String.format(sub, args)); - } catch (Exception e) { - logger.trace(String.format("start search elaboration for: %s", sub)); + // Phase 1: try regex matching with formatted string (guarded by length limit) + if (args != null && args.length > 0) { + try { + String formatted = String.format(sub, args); + if (formatted.length() <= maxElaborationRegex) { + err = findMostSimilarRegex(formatted); + } + } catch (Exception e) { + logger.trace(String.format("failed to format for regex matching: %s", e.getMessage())); + } + } + + // Phase 2: if formatted string missed or was too long, fallback to raw fmt template + if (err == null) { err = findMostSimilarRegex(sub); } - // find by distance is not reliable disable it for now + // Phase 3: distance matching uses the raw fmt template (always short) if (err == null) { err = findSimilarDistance(sub); } @@ -355,6 +401,10 @@ private static boolean verifyElaboration(ErrorCodeElaboration elaboration, Strin // better precision, worse performance private static ErrorCodeElaboration findMostSimilarRegex(String sub) { + if (sub.length() > maxElaborationRegex) { + return null; + } + if (!isRegexMatchedByRetrees(sub)) { return null; }