From 1a49556dab0f098196afd628bbe5ac83d6107760 Mon Sep 17 00:00:00 2001 From: "shixin.ruan" Date: Thu, 5 Feb 2026 19:43:22 +0800 Subject: [PATCH] [network]: set nic ip out of l3 cidr scope APIImpact DBImpact GlobalConfigImpact Resolves: ZSTAC-81969 Change-Id: I736e77646266696e646271766d7170627378796b --- .gitignore | 1 + .../zstack/compute/vm/StaticIpOperator.java | 188 +++++- .../zstack/compute/vm/VmCascadeExtension.java | 8 +- .../org/zstack/compute/vm/VmGlobalConfig.java | 4 + .../compute/vm/VmInstanceApiInterceptor.java | 34 +- .../org/zstack/compute/vm/VmInstanceBase.java | 82 ++- .../org/zstack/compute/vm/VmSystemTags.java | 5 + conf/db/upgrade/V5.5.7__schema.sql | 40 ++ conf/globalConfig/vm.xml | 8 + .../header/network/l3/UsedIpInventory.java | 10 + .../zstack/header/network/l3/UsedIpVO.java | 13 +- .../zstack/header/network/l3/UsedIpVO_.java | 2 + .../header/vm/APIChangeVmNicNetworkMsg.java | 72 +++ .../zstack/header/vm/APISetVmStaticIpMsg.java | 12 + .../header/vm/ChangeVmNicNetworkMsg.java | 63 ++ .../zstack/header/vm/SetVmStaticIpMsg.java | 11 + ...VmNicIpChangedForNoIpamExtensionPoint.java | 10 + .../network/l3/CHANGES-VmNicIpOutsideCidr.md | 359 ++++++++++ .../network/l3/L3NetworkApiInterceptor.java | 42 ++ .../network/l3/L3NetworkManagerImpl.java | 76 ++- .../network/l3/L3NetworkSystemTags.java | 1 - .../network/l3/NormalIpRangeFactory.java | 4 +- .../zstack/network/service/DhcpExtension.java | 104 ++- .../service/NetworkServiceManager.java | 10 + .../service/NetworkServiceManagerImpl.java | 24 + .../zstack/appliancevm/ApplianceVmNicTO.java | 16 +- .../service/eip/EipApiInterceptor.java | 8 + .../network/service/flat/FlatEipBackend.java | 11 +- .../lb/LoadBalancerApiInterceptor.java | 13 + .../PortForwardingApiInterceptor.java | 12 + .../SecurityGroupManagerImpl.java | 4 +- .../service/virtualrouter/VirtualRouter.java | 16 +- .../VirtualRouterManagerImpl.java | 13 +- .../flat/FlatChangeVmIpOutsideCidrCase.groovy | 612 ++++++++++++++++++ .../utils/network/NicIpAddressInfo.java | 16 + 35 files changed, 1791 insertions(+), 113 deletions(-) create mode 100644 conf/db/upgrade/V5.5.7__schema.sql create mode 100644 header/src/main/java/org/zstack/header/vm/VmNicIpChangedForNoIpamExtensionPoint.java create mode 100644 network/src/main/java/org/zstack/network/l3/CHANGES-VmNicIpOutsideCidr.md create mode 100644 test/src/test/groovy/org/zstack/test/integration/networkservice/provider/flat/FlatChangeVmIpOutsideCidrCase.groovy diff --git a/.gitignore b/.gitignore index 641f731fe03..65107cb1ae8 100755 --- a/.gitignore +++ b/.gitignore @@ -34,3 +34,4 @@ envDSLTree test/zstack-integration-test-result/ premium/test-premium/zstack-api.log **/bin/ +CLAUDE.md 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..14f80e042af 100755 --- a/compute/src/main/java/org/zstack/compute/vm/StaticIpOperator.java +++ b/compute/src/main/java/org/zstack/compute/vm/StaticIpOperator.java @@ -30,6 +30,7 @@ import javax.persistence.Tuple; import java.util.ArrayList; +import java.util.Arrays; import java.util.HashMap; import java.util.List; import java.util.Map; @@ -109,30 +110,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 +235,76 @@ public void deleteStaticIpByL3NetworkUuid(String l3Uuid) { ))); } + public void setStaticDns(String vmUuid, String l3Uuid, List dnsAddresses) { + if (dnsAddresses == null || dnsAddresses.isEmpty()) { + deleteStaticDnsByVmUuidAndL3Uuid(vmUuid, l3Uuid); + return; + } + + // Validate DNS addresses + for (String dns : dnsAddresses) { + if (!NetworkUtils.isIpv4Address(dns) && !IPv6NetworkUtils.isIpv6Address(dns)) { + throw new ApiMessageInterceptionException(argerr( + "invalid DNS address[%s], must be a valid IPv4 or IPv6 address", dns)); + } + } + + // 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 List getStaticDnsByVmUuidAndL3Uuid(String vmUuid, String l3Uuid) { + List> tokenList = VmSystemTags.STATIC_DNS.getTokensOfTagsByResourceUuid(vmUuid); + for (Map tokens : tokenList) { + String uuid = tokens.get(VmSystemTags.STATIC_DNS_L3_UUID_TOKEN); + if (uuid.equals(l3Uuid)) { + String dnsStr = tokens.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)); + } + return dnsList; + } + } + } + return null; + } + public Map getNicStaticIpMap(List nicStaticIpList) { Map nicStaticIpMap = new HashMap<>(); if (nicStaticIpList != null) { @@ -264,6 +347,11 @@ public boolean isIpChange(String vmUuid, String l3Uuid) { } public Boolean checkIpRangeConflict(VmNicVO nicVO){ + // If global config allows IP outside range, skip the conflict check + if (VmGlobalConfig.ALLOW_IP_OUTSIDE_RANGE.value(Boolean.class)) { + return Boolean.FALSE; + } + if (Q.New(IpRangeVO.class).eq(IpRangeVO_.l3NetworkUuid, nicVO.getL3NetworkUuid()).list().isEmpty()) { return Boolean.FALSE; } @@ -321,6 +409,8 @@ public void validateSystemTagInCreateMessage(APICreateMessage msg) { public List fillUpStaticIpInfoToVmNics(Map staticIps) { List newSystags = new ArrayList<>(); + boolean allowOutsideRange = VmGlobalConfig.ALLOW_IP_OUTSIDE_RANGE.value(Boolean.class); + for (Map.Entry e : staticIps.entrySet()) { String l3Uuid = e.getKey(); NicIpAddressInfo nicIp = e.getValue(); @@ -334,66 +424,112 @@ public List fillUpStaticIpInfoToVmNics(Map sta } if (!StringUtils.isEmpty(nicIp.ipv4Address)) { - NormalIpRangeVO ipRangeVO = Q.New(NormalIpRangeVO.class) + List ipRangeVOs = Q.New(NormalIpRangeVO.class) .eq(NormalIpRangeVO_.l3NetworkUuid, l3Uuid) .eq(NormalIpRangeVO_.ipVersion, IPv6Constants.IPv4) - .limit(1).find(); - if (ipRangeVO == null) { + .list(); + + // Check if IP is within any of the ranges + NormalIpRangeVO matchedIpRange = null; + for (NormalIpRangeVO ipr : ipRangeVOs) { + if (NetworkUtils.isInRange(nicIp.ipv4Address, ipr.getStartIp(), ipr.getEndIp())) { + matchedIpRange = ipr; + break; + } + } + boolean ipInRange = matchedIpRange != null; + + if (!ipInRange && !allowOutsideRange && !ipRangeVOs.isEmpty()) { + throw new ApiMessageInterceptionException(argerr( + "IP[%s] is not in any IP range of L3 network[uuid:%s] and vm.allowIpOutsideRange is disabled", + nicIp.ipv4Address, l3Uuid)); + } + + if (ipRangeVOs.isEmpty() || (allowOutsideRange && !ipInRange)) { + // No IP range or IP is outside range with allowOutsideRange enabled + // User must provide netmask and gateway if (StringUtils.isEmpty(nicIp.ipv4Netmask)) { - throw new ApiMessageInterceptionException(operr(ORG_ZSTACK_COMPUTE_VM_10310, "netmask must be set")); + throw new ApiMessageInterceptionException(operr(ORG_ZSTACK_COMPUTE_VM_10310, "netmask must be set for IP outside range")); + } + if (StringUtils.isEmpty(nicIp.ipv4Gateway)) { + throw new ApiMessageInterceptionException(operr(ORG_ZSTACK_COMPUTE_VM_10312, "gateway must be set for IP outside range")); } } else { + // IP is within range, use IpRange values or validate user-provided values 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())) + e(VmSystemTags.IPV4_NETMASK_TOKEN, matchedIpRange.getNetmask())) )); - } else if (!nicIp.ipv4Netmask.equals(ipRangeVO.getNetmask())) { + } else if (!nicIp.ipv4Netmask.equals(matchedIpRange.getNetmask())) { throw new ApiMessageInterceptionException(operr(ORG_ZSTACK_COMPUTE_VM_10311, "netmask error, expect: %s, got: %s", - ipRangeVO.getNetmask(), nicIp.ipv4Netmask)); + matchedIpRange.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())) + e(VmSystemTags.IPV4_GATEWAY_TOKEN, matchedIpRange.getGateway())) )); - } else if (!nicIp.ipv4Gateway.equals(ipRangeVO.getGateway())) { + } else if (!nicIp.ipv4Gateway.equals(matchedIpRange.getGateway())) { throw new ApiMessageInterceptionException(operr(ORG_ZSTACK_COMPUTE_VM_10312, "gateway error, expect: %s, got: %s", - ipRangeVO.getGateway(), nicIp.ipv4Gateway)); + matchedIpRange.getGateway(), nicIp.ipv4Gateway)); } } } if (!StringUtils.isEmpty(nicIp.ipv6Address)) { - NormalIpRangeVO ipRangeVO = Q.New(NormalIpRangeVO.class) + List ipv6RangeVOs = Q.New(NormalIpRangeVO.class) .eq(NormalIpRangeVO_.l3NetworkUuid, l3Uuid) .eq(NormalIpRangeVO_.ipVersion, IPv6Constants.IPv6) - .limit(1).find(); - if (ipRangeVO == null) { + .list(); + + // Check if IPv6 is within any of the ranges + NormalIpRangeVO matchedIpv6Range = null; + for (NormalIpRangeVO ipr : ipv6RangeVOs) { + if (IPv6NetworkUtils.isIpv6InRange(nicIp.ipv6Address, ipr.getStartIp(), ipr.getEndIp())) { + matchedIpv6Range = ipr; + break; + } + } + boolean ipInRange = matchedIpv6Range != null; + + if (!ipInRange && !allowOutsideRange && !ipv6RangeVOs.isEmpty()) { + throw new ApiMessageInterceptionException(argerr( + "IPv6[%s] is not in any IP range of L3 network[uuid:%s] and vm.allowIpOutsideRange is disabled", + nicIp.ipv6Address, l3Uuid)); + } + + if (ipv6RangeVOs.isEmpty() || (allowOutsideRange && !ipInRange)) { + // No IP range or IP is outside range with allowOutsideRange enabled + // User must provide prefixLen and gateway if (StringUtils.isEmpty(nicIp.ipv6Prefix)) { - throw new ApiMessageInterceptionException(operr(ORG_ZSTACK_COMPUTE_VM_10313, "ipv6 prefix length must be set")); + throw new ApiMessageInterceptionException(operr(ORG_ZSTACK_COMPUTE_VM_10313, "ipv6 prefix length must be set for IP outside range")); + } + if (StringUtils.isEmpty(nicIp.ipv6Gateway)) { + throw new ApiMessageInterceptionException(operr(ORG_ZSTACK_COMPUTE_VM_10315, "ipv6 gateway must be set for IP outside range")); } } else { + // IP is within range, use IpRange values or validate user-provided values 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())) + e(VmSystemTags.IPV6_PREFIX_TOKEN, matchedIpv6Range.getPrefixLen())) )); - } else if (!nicIp.ipv6Prefix.equals(ipRangeVO.getPrefixLen().toString())) { + } else if (!nicIp.ipv6Prefix.equals(matchedIpv6Range.getPrefixLen().toString())) { throw new ApiMessageInterceptionException(operr(ORG_ZSTACK_COMPUTE_VM_10314, "ipv6 prefix length error, expect: %s, got: %s", - ipRangeVO.getPrefixLen(), nicIp.ipv6Prefix)); + matchedIpv6Range.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()))) + IPv6NetworkUtils.ipv6AddressToTagValue(matchedIpv6Range.getGateway()))) )); - } else if (!nicIp.ipv6Gateway.equals(ipRangeVO.getGateway())) { + } else if (!nicIp.ipv6Gateway.equals(matchedIpv6Range.getGateway())) { throw new ApiMessageInterceptionException(operr(ORG_ZSTACK_COMPUTE_VM_10315, "gateway error, expect: %s, got: %s", - ipRangeVO.getGateway(), nicIp.ipv6Gateway)); + matchedIpv6Range.getGateway(), nicIp.ipv6Gateway)); } } } 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> staticIps = new StaticIpOperator().getStaticIpbySystemTag(msg.getSystemTags()); - if (msg.getRequiredIpMap() != null) { + if (msg.getStaticIp() != 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()); @@ -358,8 +356,8 @@ private void validate(APIChangeVmNicNetworkMsg msg) { } SimpleQuery uq = dbf.createQuery(UsedIpVO.class); - uq.add(UsedIpVO_.l3NetworkUuid, Op.EQ, msg.getDestL3NetworkUuid()); - uq.add(UsedIpVO_.ip, Op.EQ, msg.getStaticIp()); + uq.add(UsedIpVO_.l3NetworkUuid, Op.EQ, l3Uuid); + uq.add(UsedIpVO_.ip, Op.EQ, staticIp); 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)); } @@ -372,15 +370,11 @@ private void validate(APIChangeVmNicNetworkMsg msg) { msg.getRequiredIpMap().put(e.getKey(), e.getValue()); } - 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 (msg.getIp() != null && !msg.getIp().isEmpty() && Q.New(UsedIpVO.class).eq(UsedIpVO_.ip, msg.getIp()).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]", msg.getIp(), msg.getDestL3NetworkUuid())); + } + if (msg.getIp6() != null && !msg.getIp6().isEmpty() && Q.New(UsedIpVO.class).eq(UsedIpVO_.ip, IPv6NetworkUtils.getIpv6AddressCanonicalString(msg.getIp6())).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]", msg.getIp6(), msg.getDestL3NetworkUuid())); } } @@ -588,8 +582,12 @@ private void validateStaticIPv4(VmNicVO vmNicVO, L3NetworkVO l3NetworkVO, String continue; } // check if the ip is in the ip range when ipam is enabled + if (ipVo.getIpRangeUuid() == null) { + // IP is outside range, skip range validation + continue; + } NormalIpRangeVO rangeVO = dbf.findByUuid(ipVo.getIpRangeUuid(), NormalIpRangeVO.class); - if (!NetworkUtils.isIpv4InCidr(ip, rangeVO.getNetworkCidr())) { + if (rangeVO != null && !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())); } @@ -615,8 +613,12 @@ private void validateStaticIPv6(VmNicVO vmNicVO, L3NetworkVO l3NetworkVO, String if (!l3NetworkVO.enableIpAddressAllocation()) { continue; } + if (ipVo.getIpRangeUuid() == null) { + // IP is outside range, skip range validation + continue; + } NormalIpRangeVO rangeVO = dbf.findByUuid(ipVo.getIpRangeUuid(), NormalIpRangeVO.class); - if (!IPv6NetworkUtils.isIpv6InRange(ip, rangeVO.getStartIp(), rangeVO.getEndIp())) { + if (rangeVO != null && !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())); } 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 d8c8be65325..3b765d8a7d2 100755 --- a/compute/src/main/java/org/zstack/compute/vm/VmInstanceBase.java +++ b/compute/src/main/java/org/zstack/compute/vm/VmInstanceBase.java @@ -3475,6 +3475,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 @@ -3646,6 +3647,17 @@ 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()); + } + + if (self.getState() == VmInstanceState.Running) { + CollectionUtils.safeForEach( + pluginRgty.getExtensionList(VmNicIpChangedForNoIpamExtensionPoint.class), + ext -> ext.afterVmNicIpChangedForNoIpam(self.getUuid(), vmNicVO.getUuid())); + } + completion.success(); } }); @@ -3684,6 +3696,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 +5447,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 +6206,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); } @@ -6218,7 +6236,14 @@ private void handle(APIChangeVmNicNetworkMsg msg) { cmsg.setStaticIp(msg.getStaticIp()); cmsg.setVmInstanceUuid(msg.getVmInstanceUuid()); cmsg.setRequiredIpMap(msg.getRequiredIpMap()); + cmsg.setIp(msg.getIp()); + cmsg.setIp6(msg.getIp6()); + cmsg.setNetmask(msg.getNetmask()); + cmsg.setGateway(msg.getGateway()); + cmsg.setIpv6Gateway(msg.getIpv6Gateway()); + cmsg.setIpv6Prefix(msg.getIpv6Prefix()); cmsg.setSystemTags(msg.getSystemTags()); + cmsg.setDnsAddresses(msg.getDnsAddresses()); bus.makeTargetServiceIdByResourceUuid(cmsg, VmInstanceConstant.SERVICE_ID, cmsg.getVmInstanceUuid()); bus.send(cmsg, new CloudBusCallBack(msg) { @Override @@ -6246,6 +6271,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 +6292,28 @@ void set() { isSet = true; } + void setDns() { + if (msg.getDnsAddresses() != null && !msg.getDnsAddresses().isEmpty()) { + 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() { @@ -6373,43 +6410,43 @@ public void run(FlowTrigger trigger, Map data) { } self = dbf.updateAndRefresh(self); VmNicVO nicVO = dbf.findByUuid(nic.getUuid(), VmNicVO.class); - final Map nicNetworkInfo = new StaticIpOperator().getNicNetworkInfoBySystemTag(msg.getSystemTags()); List voNewList = new ArrayList<>(); List voOldList = Q.New(UsedIpVO.class).eq(UsedIpVO_.vmNicUuid, nicVO.getUuid()).list(); - NicIpAddressInfo nicIpAddressInfo = nicNetworkInfo.get(msg.getDestL3NetworkUuid()); - if (nicIpAddressInfo == null) { + boolean hasIpInfo = (msg.getIp() != null && !msg.getIp().isEmpty()) + || (msg.getIp6() != null && !msg.getIp6().isEmpty()); + if (!hasIpInfo) { nicVO.setUsedIpUuid(null); nicVO.setIp(null); nicVO.setGateway(null); nicVO.setNetmask(null); nicVO.setL3NetworkUuid(msg.getDestL3NetworkUuid()); } else { - if (nicIpAddressInfo.ipv6Address != null && !nicIpAddressInfo.ipv6Address.isEmpty()) { + if (msg.getIp6() != null && !msg.getIp6().isEmpty()) { UsedIpVO vo = new UsedIpVO(); vo.setUuid(Platform.getUuid()); - vo.setIp(IPv6NetworkUtils.getIpv6AddressCanonicalString(nicIpAddressInfo.ipv6Address)); - vo.setNetmask(IPv6NetworkUtils.getFormalNetmaskOfNetworkCidr(nicIpAddressInfo.ipv6Address + "/" + nicIpAddressInfo.ipv6Prefix)); - vo.setGateway(nicIpAddressInfo.ipv6Gateway.isEmpty() ? "" : IPv6NetworkUtils.getIpv6AddressCanonicalString(nicIpAddressInfo.ipv6Gateway)); + vo.setIp(IPv6NetworkUtils.getIpv6AddressCanonicalString(msg.getIp6())); + vo.setNetmask(IPv6NetworkUtils.getFormalNetmaskOfNetworkCidr(msg.getIp6() + "/" + msg.getIpv6Prefix())); + vo.setGateway(msg.getIpv6Gateway() == null || msg.getIpv6Gateway().isEmpty() ? "" : IPv6NetworkUtils.getIpv6AddressCanonicalString(msg.getIpv6Gateway())); vo.setIpVersion(IPv6Constants.IPv6); vo.setVmNicUuid(msg.getVmNicUuid()); vo.setL3NetworkUuid(msg.getDestL3NetworkUuid()); vo.setIpRangeUuid(new StaticIpOperator().getIpRangeUuid(vo.getL3NetworkUuid(), vo.getIp())); vo.setIpInBinary(NetworkUtils.ipStringToBytes(vo.getIp())); nicVO.setUsedIpUuid(vo.getUuid()); - nicVO.setIp(nicIpAddressInfo.ipv4Address); - nicVO.setGateway(nicIpAddressInfo.ipv4Gateway); - nicVO.setNetmask(nicIpAddressInfo.ipv4Netmask); + nicVO.setIp(msg.getIp()); + nicVO.setGateway(msg.getGateway()); + nicVO.setNetmask(msg.getNetmask()); nicVO.setL3NetworkUuid(msg.getDestL3NetworkUuid()); voNewList.add(vo); } - if (nicIpAddressInfo.ipv4Address != null && !nicIpAddressInfo.ipv4Address.isEmpty()) { + if (msg.getIp() != null && !msg.getIp().isEmpty()) { UsedIpVO vo = new UsedIpVO(); vo.setUuid(Platform.getUuid()); - if (NetworkUtils.isIpv4Address(nicIpAddressInfo.ipv4Address)) { - vo.setIpInLong(NetworkUtils.ipv4StringToLong(nicIpAddressInfo.ipv4Address)); - vo.setIp(nicIpAddressInfo.ipv4Address); - vo.setNetmask(nicIpAddressInfo.ipv4Netmask); - vo.setGateway(nicIpAddressInfo.ipv4Gateway); + if (NetworkUtils.isIpv4Address(msg.getIp())) { + vo.setIpInLong(NetworkUtils.ipv4StringToLong(msg.getIp())); + vo.setIp(msg.getIp()); + vo.setNetmask(msg.getNetmask()); + vo.setGateway(msg.getGateway()); vo.setIpVersion(IPv6Constants.IPv4); vo.setVmNicUuid(msg.getVmNicUuid()); vo.setL3NetworkUuid(msg.getDestL3NetworkUuid()); @@ -6417,9 +6454,9 @@ public void run(FlowTrigger trigger, Map data) { vo.setIpInBinary(NetworkUtils.ipStringToBytes(vo.getIp())); vo.setIpRangeUuid(new StaticIpOperator().getIpRangeUuid(vo.getL3NetworkUuid(), vo.getIp())); nicVO.setUsedIpUuid(vo.getUuid()); - nicVO.setIp(nicIpAddressInfo.ipv4Address); - nicVO.setGateway(nicIpAddressInfo.ipv4Gateway); - nicVO.setNetmask(nicIpAddressInfo.ipv4Netmask); + nicVO.setIp(msg.getIp()); + nicVO.setGateway(msg.getGateway()); + nicVO.setNetmask(msg.getNetmask()); nicVO.setL3NetworkUuid(msg.getDestL3NetworkUuid()); voNewList.add(vo); } @@ -6562,6 +6599,13 @@ public void run(FlowTrigger trigger, Map data) { @Override public void handle(Map data) { VmNicVO nicVO = (VmNicVO) data.get(VmInstanceConstant.Params.VmNicInventory.toString()); + + if (!destL3.enableIpAddressAllocation() && self.getState() == VmInstanceState.Running) { + CollectionUtils.safeForEach( + pluginRgty.getExtensionList(VmNicIpChangedForNoIpamExtensionPoint.class), + ext -> ext.afterVmNicIpChangedForNoIpam(self.getUuid(), nicVO.getUuid())); + } + completion.success(VmNicInventory.valueOf(nicVO)); chain.next(); } 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.7__schema.sql b/conf/db/upgrade/V5.5.7__schema.sql new file mode 100644 index 00000000000..921b8cafde6 --- /dev/null +++ b/conf/db/upgrade/V5.5.7__schema.sql @@ -0,0 +1,40 @@ +-- 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 UsedIpVO u +INNER JOIN 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) +-- This allows UsedIpVO records to exist without an IpRange (for IPs outside range) +DELIMITER $$ + +CREATE PROCEDURE ModifyUsedIpVOForeignKey() +BEGIN + DECLARE constraint_exists INT; + + -- Check if the constraint exists + SELECT COUNT(*) + INTO constraint_exists + FROM INFORMATION_SCHEMA.TABLE_CONSTRAINTS + WHERE TABLE_SCHEMA = 'zstack' + AND TABLE_NAME = 'UsedIpVO' + AND CONSTRAINT_NAME = 'fkUsedIpVOIpRangeEO'; + + IF constraint_exists > 0 THEN + -- Drop the existing constraint + ALTER TABLE `zstack`.`UsedIpVO` DROP FOREIGN KEY `fkUsedIpVOIpRangeEO`; + + -- Re-create with SET NULL on delete + ALTER TABLE `zstack`.`UsedIpVO` + ADD CONSTRAINT `fkUsedIpVOIpRangeEO` + FOREIGN KEY (`ipRangeUuid`) REFERENCES `IpRangeEO`(`uuid`) ON DELETE SET NULL; + END IF; +END $$ + +DELIMITER ; + +CALL ModifyUsedIpVOForeignKey(); +DROP PROCEDURE IF EXISTS ModifyUsedIpVOForeignKey; diff --git a/conf/globalConfig/vm.xml b/conf/globalConfig/vm.xml index a7f835c82ee..64be40b57f1 100755 --- a/conf/globalConfig/vm.xml +++ b/conf/globalConfig/vm.xml @@ -310,6 +310,14 @@ false + + vm + allow.ip.outside.range + Allow VM NIC to use IP address outside of L3 network IP ranges. When enabled, users must provide netmask/gateway for IPv4 or prefixLen/gateway for IPv6. + java.lang.Boolean + true + + vm uniqueVmName 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/vm/APIChangeVmNicNetworkMsg.java b/header/src/main/java/org/zstack/header/vm/APIChangeVmNicNetworkMsg.java index c00ab47e904..0f4b4ef86df 100644 --- a/header/src/main/java/org/zstack/header/vm/APIChangeVmNicNetworkMsg.java +++ b/header/src/main/java/org/zstack/header/vm/APIChangeVmNicNetworkMsg.java @@ -33,6 +33,22 @@ public class APIChangeVmNicNetworkMsg extends APIMessage implements VmInstanceMe private String staticIp; + @APIParam(required = false) + private String ip; + @APIParam(required = false) + private String ip6; + @APIParam(required = false) + private String netmask; + @APIParam(required = false) + private String gateway; + @APIParam(required = false) + private String ipv6Gateway; + @APIParam(required = false) + private String ipv6Prefix; + + @APIParam(required = false) + private List dnsAddresses; + public String getVmNicUuid() { return vmNicUuid; } @@ -57,6 +73,54 @@ public void setRequiredIpMap(Map> requiredIpMap) { this.requiredIpMap = requiredIpMap; } + public String getIp() { + return ip; + } + + public void setIp(String ip) { + this.ip = ip; + } + + public String getIp6() { + return ip6; + } + + public void setIp6(String ip6) { + this.ip6 = ip6; + } + + 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; + } + public static APIChangeVmNicNetworkMsg __example__() { APIChangeVmNicNetworkMsg msg = new APIChangeVmNicNetworkMsg(); msg.vmNicUuid = uuid(); @@ -80,4 +144,12 @@ 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/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/ChangeVmNicNetworkMsg.java b/header/src/main/java/org/zstack/header/vm/ChangeVmNicNetworkMsg.java index 1e7f7b1772f..ced3f822f90 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,13 @@ public class ChangeVmNicNetworkMsg extends NeedReplyMessage implements VmInstanc private String vmInstanceUuid; private Map> requiredIpMap; private String staticIp; + private String ip; + private String ip6; + private String netmask; + private String gateway; + private String ipv6Gateway; + private String ipv6Prefix; + private List dnsAddresses; public String getVmNicUuid() { return vmNicUuid; @@ -55,4 +62,60 @@ public String getStaticIp() { public void setStaticIp(String staticIp) { this.staticIp = staticIp; } + + public String getIp() { + return ip; + } + + public void setIp(String ip) { + this.ip = ip; + } + + public String getIp6() { + return ip6; + } + + public void setIp6(String ip6) { + this.ip6 = ip6; + } + + 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; + } + + 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/VmNicIpChangedForNoIpamExtensionPoint.java b/header/src/main/java/org/zstack/header/vm/VmNicIpChangedForNoIpamExtensionPoint.java new file mode 100644 index 00000000000..da7ee50fb7b --- /dev/null +++ b/header/src/main/java/org/zstack/header/vm/VmNicIpChangedForNoIpamExtensionPoint.java @@ -0,0 +1,10 @@ +package org.zstack.header.vm; + +/** + * Extension point called after VM NIC IP configuration changes in disable-IPAM scenarios + * (where ipRangeUuid is null). This allows modules like GuestTools to sync the updated + * network config to the VM via QGA, since DHCP path skips IPs without ipRangeUuid. + */ +public interface VmNicIpChangedForNoIpamExtensionPoint { + void afterVmNicIpChangedForNoIpam(String vmInstanceUuid, String vmNicUuid); +} diff --git a/network/src/main/java/org/zstack/network/l3/CHANGES-VmNicIpOutsideCidr.md b/network/src/main/java/org/zstack/network/l3/CHANGES-VmNicIpOutsideCidr.md new file mode 100644 index 00000000000..b8aefeed829 --- /dev/null +++ b/network/src/main/java/org/zstack/network/l3/CHANGES-VmNicIpOutsideCidr.md @@ -0,0 +1,359 @@ +# VM网卡IP地址支持不在L3 CIDR范围内 - 修改总结 + +## 功能概述 + +支持云主机网卡配置不在L3网络IP Range范围内的IP地址,同时支持通过QGA设置自定义DNS服务器。 + +## 一、DNS功能实现 + +### 1.1 系统标签定义 +**文件**: `compute/src/main/java/org/zstack/compute/vm/VmSystemTags.java` +```java +// 新增DNS系统标签 +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); +``` + +### 1.2 API消息修改 +**文件**: `header/src/main/java/org/zstack/header/vm/APISetVmStaticIpMsg.java` +```java +@APIParam(required = false) +private List dnsAddresses; +``` + +**文件**: `header/src/main/java/org/zstack/header/vm/SetVmStaticIpMsg.java` +```java +private List dnsAddresses; +``` + +**文件**: `header/src/main/java/org/zstack/header/vm/APIChangeVmNicNetworkMsg.java` +```java +@APIParam(required = false) +private String ip; +@APIParam(required = false) +private String ip6; +@APIParam(required = false) +private String netmask; +@APIParam(required = false) +private String gateway; +@APIParam(required = false) +private String ipv6Gateway; +@APIParam(required = false) +private String ipv6Prefix; + +@APIParam(required = false) +private List dnsAddresses; +``` + +**文件**: `header/src/main/java/org/zstack/header/vm/ChangeVmNicNetworkMsg.java` +```java +private String ip; +private String ip6; +private String netmask; +private String gateway; +private String ipv6Gateway; +private String ipv6Prefix; +private List dnsAddresses; +``` + +### 1.3 DNS操作方法 +**文件**: `compute/src/main/java/org/zstack/compute/vm/StaticIpOperator.java` +```java +// 设置静态DNS +public void setStaticDns(String vmUuid, String l3Uuid, List dnsAddresses) + +// 删除静态DNS +public void deleteStaticDnsByVmUuidAndL3Uuid(String vmUuid, String l3Uuid) + +// 获取静态DNS +public List getStaticDnsByVmUuidAndL3Uuid(String vmUuid, String l3Uuid) +``` + +### 1.4 DNS获取接口 +**文件**: `network/src/main/java/org/zstack/network/service/NetworkServiceManager.java` +```java +// 新增接口方法 +List getVmNicDns(String vmUuid, String l3NetworkUuid); +``` + +**文件**: `network/src/main/java/org/zstack/network/service/NetworkServiceManagerImpl.java` +```java +@Override +public List getVmNicDns(String vmUuid, String l3NetworkUuid) { + // 优先返回系统标签DNS,否则返回L3网络DNS + List customDns = new StaticIpOperator().getStaticDnsByVmUuidAndL3Uuid(vmUuid, l3NetworkUuid); + if (customDns != null && !customDns.isEmpty()) { + return customDns; + } + return getL3NetworkDns(l3NetworkUuid); +} +``` + +### 1.5 VM实例处理 +**文件**: `compute/src/main/java/org/zstack/compute/vm/VmInstanceBase.java` +- `handle(APISetVmStaticIpMsg)` - 传递dnsAddresses +- `setIpamStaticIp()` - 调用setStaticDns() +- `setNoIpamStaticIp()` - 调用setStaticDns() +- `handle(APIChangeVmNicNetworkMsg)` - 传递dnsAddresses +- `changeVmNicNetwork()` - SetStaticIp内部类处理DNS + +### 1.6 GuestTools集成 +**文件**: `premium/guesttools/src/main/java/org/zstack/guesttools/GuestToolsManagerImpl.java` +```java +// 使用getVmNicDns()替代getL3NetworkDns() +ipConfig.setDns(nwServiceMgr.getVmNicDns(vmUuid, nic.getL3NetworkUuid())); +``` + +--- + +## 二、IP地址不在L3 CIDR范围内的约束实现 + +### 2.1 L3网络IP统计 +**文件**: `network/src/main/java/org/zstack/network/l3/L3NetworkManagerImpl.java` + +**修改**: 按L3Network和Zone统计UsedIp时排除`ipRangeUuid=null`的记录 +```sql +-- 修改前 +select count(distinct uip.ip), uip.l3NetworkUuid, uip.ipVersion +from UsedIpVO uip where uip.l3NetworkUuid in (:uuids) ... + +-- 修改后 +select count(distinct uip.ip), uip.l3NetworkUuid, uip.ipVersion +from UsedIpVO uip where uip.l3NetworkUuid in (:uuids) +and uip.ipRangeUuid is not null ... +``` + +### 2.2 添加IpRange时更新孤儿IP +**文件**: `network/src/main/java/org/zstack/network/l3/NormalIpRangeFactory.java` + +**修改**: 只查询`ipRangeUuid=null`的UsedIpVO进行更新 +```java +List usedIpVos = Q.New(UsedIpVO.class) + .eq(UsedIpVO_.l3NetworkUuid, vo.getL3NetworkUuid()) + .eq(UsedIpVO_.ipVersion, vo.getIpVersion()) + .isNull(UsedIpVO_.ipRangeUuid) // 新增条件 + .list(); +``` + +### 2.3 添加IpRange时校验特殊地址 +**文件**: `network/src/main/java/org/zstack/network/l3/L3NetworkApiInterceptor.java` + +**新增**: 添加第一个IpRange时校验gateway/network/broadcast地址未被占用 +```java +// 当添加第一个IpRange时,检查这些地址是否已被使用 +if (l3IpRanges.isEmpty()) { + // 检查gateway地址 + 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(...); + } + // 检查network地址和broadcast地址... +} +``` + +### 2.4 DHCP配置排除CIDR外地址 +**文件**: `network/src/main/java/org/zstack/network/service/DhcpExtension.java` + +**修改1**: `isDualStackNicInSingleL3Network()`方法过滤`ipRangeUuid=null` +```java +List validIps = nic.getUsedIps().stream() + .filter(ip -> ip.getIpRangeUuid() != null) + .collect(Collectors.toList()); +``` + +**修改2**: `setDualStackNicOfSingleL3Network()`方法过滤`ipRangeUuid=null` + +**修改3**: `makeDhcpStruct()`主循环跳过`ipRangeUuid=null` +```java +for (UsedIpVO ip : nic.getUsedIps()) { + if (ip.getIpRangeUuid() == null) { + logger.debug("skip DHCP for vmnic[ip:%s] because it's outside L3 CIDR range"); + continue; + } + // ... +} +``` + +### 2.5 安全组计算排除CIDR外地址 +**文件**: `plugin/securityGroup/src/main/java/org/zstack/network/securitygroup/SecurityGroupManagerImpl.java` + +**修改**: `getVmIpsBySecurityGroup()`方法SQL添加`ip.ipRangeUuid is not null` +```sql +select ip.ip from VmNicVO nic, VmNicSecurityGroupRefVO ref, SecurityGroupVO sg, UsedIpVO ip +where ... and ip.ipRangeUuid is not null +``` + +### 2.6 EIP禁止绑定CIDR外地址 +**文件**: `plugin/eip/src/main/java/org/zstack/network/service/eip/EipApiInterceptor.java` + +**新增**: `validate(APIAttachEipMsg)`添加检查 +```java +UsedIpVO usedIpVO = dbf.findByUuid(msg.getUsedIpUuid(), UsedIpVO.class); +if (usedIpVO != null && usedIpVO.getIpRangeUuid() == null) { + throw new ApiMessageInterceptionException(argerr( + "cannot bindBind EIP to IP address[%s] which is outside L3 network CIDR range", + usedIpVO.getIp())); +} +``` + +### 2.7 PF禁止绑定CIDR外地址 +**文件**: `plugin/portForwarding/src/main/java/org/zstack/network/service/portforwarding/PortForwardingApiInterceptor.java` + +**新增**: `validate(APIAttachPortForwardingRuleMsg)`添加检查 +```java +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 && usedIpVO.getIpRangeUuid() == null) { + throw new ApiMessageInterceptionException(...); + } +} +``` + +### 2.8 LB禁止绑定CIDR外地址 +**文件**: `plugin/loadBalancer/src/main/java/org/zstack/network/service/lb/LoadBalancerApiInterceptor.java` + +**新增**: `validate(APIAddVmNicToLoadBalancerMsg)`添加检查 +```java +for (String nicUuid : msg.getVmNicUuids()) { + VmNicVO nicVO = dbf.findByUuid(nicUuid, VmNicVO.class); + if (nicVO != null && nicVO.getUsedIpUuid() != null) { + UsedIpVO usedIpVO = dbf.findByUuid(nicVO.getUsedIpUuid(), UsedIpVO.class); + if (usedIpVO != null && usedIpVO.getIpRangeUuid() == null) { + throw new ApiMessageInterceptionException(...); + } + } +} +``` + +--- + +## 三、测试用例 + +**文件**: `premium/test-premium/src/test/groovy/org/zstack/test/integration/premium/vpc/networkService/VmNicIpOutsideCidrCase.groovy` + +**测试场景**: +1. `testSetVmNicIpOutsideCidr` - 验证设置CIDR外IP时`ipRangeUuid=null` +2. `testEipCannotBindToIpOutsideCidr` - 验证EIP不能绑定 +3. `testLbCannotAddNicWithIpOutsideCidr` - 验证LB不能添加 +4. `testPfCannotBindToIpOutsideCidr` - 验证PF不能绑定 +5. `testIpStatisticsExcludeIpOutsideCidr` - 验证IP统计排除 +6. `testAddIpRangeAssociatesOrphanIps` - 验证添加IpRange后自动关联孤儿IP + +--- + +## 四、逻辑总结表 + +| 场景 | UsedIpVO.ipRangeUuid | 处理方式 | +|------|---------------------|---------| +| IP在CIDR范围内 | 有值 | 正常处理 | +| IP不在CIDR范围内 | null | 特殊处理 | +| L3网络IP统计 | - | 排除null记录 | +| DHCP下发 | - | 跳过null记录 | +| 安全组计算 | - | 排除null记录 | +| EIP/LB/PF绑定 | - | 禁止null记录 | +| 添加IpRange | - | 自动关联范围内的孤儿IP | + +--- + +## 五、全局配置 + +**文件**: `compute/src/main/java/org/zstack/compute/vm/VmGlobalConfig.java` +```java +@GlobalConfigValidation(validValues = {"true", "false"}) +@GlobalConfigDef(defaultValue = "true", type = Boolean.class, + description = "Allow VM NIC to use IP address outside of L3 network IP ranges. When enabled, users must provide netmask/gateway for IPv4 or prefixLen/gateway for IPv6.") +public static GlobalConfig ALLOW_IP_OUTSIDE_RANGE = new GlobalConfig(CATEGORY, "allow.ip.outside.range"); +``` + +**配置项**: `vm.allow.ip.outside.range` +- 默认值: `true` +- 设置为`true`时允许设置CIDR范围外的IP地址 + +--- + +## 六、APIChangeVmNicNetworkMsg 支持设置 IP/掩码/网关 + +### 6.1 消息新增显式字段(参考 APISetVmStaticIpMsg) + +**文件**: `header/src/main/java/org/zstack/header/vm/APIChangeVmNicNetworkMsg.java` +```java +@APIParam(required = false) +private String ip; +@APIParam(required = false) +private String ip6; +@APIParam(required = false) +private String netmask; +@APIParam(required = false) +private String gateway; +@APIParam(required = false) +private String ipv6Gateway; +@APIParam(required = false) +private String ipv6Prefix; +``` + +**文件**: `header/src/main/java/org/zstack/header/vm/ChangeVmNicNetworkMsg.java` +```java +private String ip; +private String ip6; +private String netmask; +private String gateway; +private String ipv6Gateway; +private String ipv6Prefix; +``` + +通过 API 成员变量直接传入 IP 地址、掩码、网关信息,不再依赖 system tags 传递。 + +### 6.2 Interceptor 校验逻辑 + +**文件**: `compute/src/main/java/org/zstack/compute/vm/VmInstanceApiInterceptor.java` + +在 `validate(APIChangeVmNicNetworkMsg)` 中,直接校验 `msg.getIp()` 和 `msg.getIp6()` 是否已被占用: +```java +if (msg.getIp() != null && !msg.getIp().isEmpty() + && Q.New(UsedIpVO.class).eq(UsedIpVO_.ip, msg.getIp()) + .eq(UsedIpVO_.l3NetworkUuid, msg.getDestL3NetworkUuid()).isExists()) { + throw new ApiMessageInterceptionException(...); +} +if (msg.getIp6() != null && !msg.getIp6().isEmpty() + && Q.New(UsedIpVO.class).eq(UsedIpVO_.ip, IPv6NetworkUtils.getIpv6AddressCanonicalString(msg.getIp6())) + .eq(UsedIpVO_.l3NetworkUuid, msg.getDestL3NetworkUuid()).isExists()) { + throw new ApiMessageInterceptionException(...); +} +``` + +### 6.3 API→内部消息传递 + +**文件**: `compute/src/main/java/org/zstack/compute/vm/VmInstanceBase.java` + +在 `handle(APIChangeVmNicNetworkMsg)` 中,逐字段传递给内部消息: +```java +ChangeVmNicNetworkMsg cmsg = new ChangeVmNicNetworkMsg(); +// ... 其他字段 ... +cmsg.setIp(msg.getIp()); +cmsg.setIp6(msg.getIp6()); +cmsg.setNetmask(msg.getNetmask()); +cmsg.setGateway(msg.getGateway()); +cmsg.setIpv6Gateway(msg.getIpv6Gateway()); +cmsg.setIpv6Prefix(msg.getIpv6Prefix()); +``` + +### 6.4 changeVmNicNetwork 处理逻辑 + +**文件**: `compute/src/main/java/org/zstack/compute/vm/VmInstanceBase.java` + +在 `changeVmNicNetwork()` 的 disable-ipam 分支中,直接从 `msg` 的显式字段读取 IP/掩码/网关,不再通过 `StaticIpOperator.getNicNetworkInfoBySystemTag()` 解析 system tags。 + +### 6.5 Bug 修复 + +**文件**: `compute/src/main/java/org/zstack/compute/vm/VmInstanceApiInterceptor.java` + +1. `getRequiredIpMap()` → `getStaticIp()`:interceptor 中处理 `staticIp` 时应使用 `msg.getStaticIp()` 而非 `msg.getRequiredIpMap()` +2. 循环变量误用修复:`for` 循环中的迭代变量与外部变量混淆导致逻辑错误 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..beab8beb380 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( + "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( + "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( + "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..64bd8edeae4 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); @@ -789,6 +790,77 @@ public UsedIpInventory reserveIp(IpRangeVO ipRange, String ip, boolean allowDupl } } + /** + * Reserve an IP address that is outside of any IP range. + * This method is used when ALLOW_IP_OUTSIDE_RANGE global config is enabled. + * + * @param l3NetworkUuid the L3 network UUID + * @param ip the IP address to reserve + * @param netmask the netmask (required for IPv4) + * @param gateway the gateway (required) + * @param prefixLen the prefix length (required for IPv6) + * @param ipVersion 4 for IPv4, 6 for IPv6 + * @return UsedIpInventory of the reserved IP + */ + public UsedIpInventory reserveIpWithoutRange(String l3NetworkUuid, String ip, String netmask, + String gateway, Integer prefixLen, int ipVersion) { + // Normalize IPv6 address before uniqueness check + String normalizedIp = ip; + if (ipVersion != IPv6Constants.IPv4) { + normalizedIp = IPv6NetworkUtils.getIpv6AddressCanonicalString(ip); + } + + // Explicit uniqueness check: ensure no UsedIpVO exists for the same IP on this L3 network. + // This covers both in-range IPs (whose UUID is derived from ipRangeUuid+ip) and + // outside-range IPs (whose UUID is derived from l3NetworkUuid+ip). + boolean exists = Q.New(UsedIpVO.class) + .eq(UsedIpVO_.l3NetworkUuid, l3NetworkUuid) + .eq(UsedIpVO_.ip, normalizedIp) + .isExists(); + if (exists) { + logger.debug(String.format("IP[%s] in L3 network[uuid:%s] has already been allocated, " + + "cannot reserve outside-range IP", ip, l3NetworkUuid)); + return null; + } + + try { + UsedIpVO vo = new UsedIpVO(); + String uuid = l3NetworkUuid + normalizedIp; + uuid = UUID.nameUUIDFromBytes(uuid.getBytes()).toString().replaceAll("-", ""); + vo.setUuid(uuid); + vo.setIpRangeUuid(null); // No IP range for outside-range IP + vo.setL3NetworkUuid(l3NetworkUuid); + vo.setIpVersion(ipVersion); + vo.setGateway(gateway); + + if (ipVersion == IPv6Constants.IPv4) { + vo.setIp(normalizedIp); + vo.setNetmask(netmask); + vo.setIpInLong(NetworkUtils.ipv4StringToLong(normalizedIp)); + vo.setIpInBinary(NetworkUtils.ipStringToBytes(normalizedIp)); + } else { + vo.setIp(normalizedIp); + vo.setNetmask(netmask); + vo.setPrefixLen(prefixLen); + vo.setIpInBinary(NetworkUtils.ipStringToBytes(normalizedIp)); + } + + vo = dbf.persistAndRefresh(vo); + logger.debug(String.format("Reserved IP[%s] outside of IP range for L3 network[uuid:%s]", ip, l3NetworkUuid)); + return UsedIpInventory.valueOf(vo); + } catch (PersistenceException e) { + if (ExceptionDSL.isCausedBy(e, SQLIntegrityConstraintViolationException.class)) { + logger.debug(String.format("Concurrent ip allocation. " + + "Ip[%s] in L3 network[uuid:%s] has been allocated. " + + "The error[Duplicate entry] printed by jdbc.spi.SqlExceptionHelper is no harm", ip, l3NetworkUuid)); + logger.trace("", e); + } else { + throw e; + } + return null; + } + } + @Override public boolean isIpRangeFull(IpRangeVO vo) { List used = getUsedIpInRange(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 782c5d96544..a1b9b3e947a 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 (ipRangeUuid is null) + List validIps = nic.getUsedIps().stream() + .filter(ip -> ip.getIpRangeUuid() != null) + .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) { @@ -188,7 +193,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 (ipRangeUuid is null) + List sortedIps = nic.getUsedIps().stream() + .filter(ip -> ip.getIpRangeUuid() != null) + .sorted(Comparator.comparingLong(UsedIpVO::getIpVersionl)) + .collect(Collectors.toList()); for (UsedIpVO ip : sortedIps) { if (ip.getIpVersion() == IPv6Constants.IPv4) { struct.setGateway(ip.getGateway()); @@ -198,19 +207,28 @@ private void setDualStackNicOfSingleL3Network(DhcpStruct struct, VmNicVO nic) { struct.setHostname(ip.getIp().replaceAll("\\.", "-")); } } else { - List iprs = Q.New(NormalIpRangeVO.class).eq(NormalIpRangeVO_.l3NetworkUuid, ip.getL3NetworkUuid()) - .eq(NormalIpRangeVO_.ipVersion, ip.getIpVersion()).list(); - struct.setGateway6(ip.getGateway()); struct.setIp6(ip.getIp()); struct.setEnableRa(isEnableRa(ip.getL3NetworkUuid())); - if (iprs.isEmpty() || iprs.get(0).getAddressMode().equals(IPv6Constants.SLAAC)) { - continue; + + // First try to use prefixLen from UsedIpVO (for IP outside range) + if (ip.getPrefixLen() != null) { + struct.setPrefixLength(ip.getPrefixLen()); + struct.setFirstIp(ip.getIp()); + struct.setEndIP(ip.getIp()); + struct.setRaMode(IPv6Constants.Stateful_DHCP); + } else { + // Fallback to IpRangeVO for IP within range + List iprs = Q.New(NormalIpRangeVO.class).eq(NormalIpRangeVO_.l3NetworkUuid, ip.getL3NetworkUuid()) + .eq(NormalIpRangeVO_.ipVersion, ip.getIpVersion()).list(); + if (iprs.isEmpty() || iprs.get(0).getAddressMode().equals(IPv6Constants.SLAAC)) { + continue; + } + struct.setRaMode(iprs.get(0).getAddressMode()); + struct.setPrefixLength(iprs.get(0).getPrefixLen()); + struct.setFirstIp(NetworkUtils.getSmallestIp(iprs.stream().map(IpRangeVO::getStartIp).collect(Collectors.toList()))); + struct.setEndIP(NetworkUtils.getBiggesttIp(iprs.stream().map(IpRangeVO::getEndIp).collect(Collectors.toList()))); } - struct.setRaMode(iprs.get(0).getAddressMode()); - struct.setPrefixLength(iprs.get(0).getPrefixLen()); - struct.setFirstIp(NetworkUtils.getSmallestIp(iprs.stream().map(IpRangeVO::getStartIp).collect(Collectors.toList()))); - struct.setEndIP(NetworkUtils.getBiggesttIp(iprs.stream().map(IpRangeVO::getEndIp).collect(Collectors.toList()))); } } } @@ -224,18 +242,29 @@ private void setNicDhcp(DhcpStruct struct, UsedIpVO ip) { struct.setHostname(ip.getIp().replaceAll("\\.", "-")); } } else { - List iprs = Q.New(NormalIpRangeVO.class).eq(NormalIpRangeVO_.l3NetworkUuid, ip.getL3NetworkUuid()) - .eq(NormalIpRangeVO_.ipVersion, IPv6Constants.IPv6).list(); struct.setGateway6(ip.getGateway()); struct.setIp6(ip.getIp()); struct.setEnableRa(isEnableRa(ip.getL3NetworkUuid())); - if (iprs.isEmpty()) { - return; + + // First try to use prefixLen from UsedIpVO (for IP outside range) + if (ip.getPrefixLen() != null) { + struct.setPrefixLength(ip.getPrefixLen()); + // For IP outside range, set firstIp and endIp to the IP itself + struct.setFirstIp(ip.getIp()); + struct.setEndIP(ip.getIp()); + struct.setRaMode(IPv6Constants.Stateful_DHCP); // Default to Stateful DHCP for outside range + } else { + // Fallback to IpRangeVO for IP within range + List iprs = Q.New(NormalIpRangeVO.class).eq(NormalIpRangeVO_.l3NetworkUuid, ip.getL3NetworkUuid()) + .eq(NormalIpRangeVO_.ipVersion, IPv6Constants.IPv6).list(); + if (iprs.isEmpty()) { + return; + } + struct.setRaMode(iprs.get(0).getAddressMode()); + struct.setPrefixLength(iprs.get(0).getPrefixLen()); + struct.setFirstIp(NetworkUtils.getSmallestIp(iprs.stream().map(IpRangeVO::getStartIp).collect(Collectors.toList()))); + struct.setEndIP(NetworkUtils.getBiggesttIp(iprs.stream().map(IpRangeVO::getEndIp).collect(Collectors.toList()))); } - struct.setRaMode(iprs.get(0).getAddressMode()); - struct.setPrefixLength(iprs.get(0).getPrefixLen()); - struct.setFirstIp(NetworkUtils.getSmallestIp(iprs.stream().map(IpRangeVO::getStartIp).collect(Collectors.toList()))); - struct.setEndIP(NetworkUtils.getBiggesttIp(iprs.stream().map(IpRangeVO::getEndIp).collect(Collectors.toList()))); } } @@ -269,19 +298,30 @@ public List makeDhcpStruct(VmInstanceInventory vm, 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..9f7255b42ec 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,11 @@ import org.zstack.header.network.service.*; import org.zstack.header.network.service.NetworkServiceExtensionPoint.NetworkServiceExtensionPosition; import org.zstack.header.vm.*; +import org.zstack.compute.vm.VmSystemTags; 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 +485,28 @@ 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) + List> tokenList = VmSystemTags.STATIC_DNS.getTokensOfTagsByResourceUuid(vmUuid); + for (Map tokens : tokenList) { + String uuid = tokens.get(VmSystemTags.STATIC_DNS_L3_UUID_TOKEN); + if (uuid.equals(l3NetworkUuid)) { + String dnsStr = tokens.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)); + } + 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..6fd109c8f02 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,19 @@ 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(); + } + 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/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..1209b8abfa7 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 @@ -202,6 +202,14 @@ public VipVO call() { } else { msg.setUsedIpUuid(nic.getUsedIpUuid()); } + + // Check if the IP is outside L3 CIDR range (ipRangeUuid is null) + UsedIpVO usedIpVO = dbf.findByUuid(msg.getUsedIpUuid(), UsedIpVO.class); + if (usedIpVO != null && usedIpVO.getIpRangeUuid() == null) { + throw new ApiMessageInterceptionException(argerr( + "cannot bind EIP to IP address[%s] which is outside L3 network CIDR range", + usedIpVO.getIp())); + } } private void validate(APIDetachEipMsg msg) { 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/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..cc6e5ce2378 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 @@ -642,6 +642,19 @@ public void run(String arg) { q = dbf.getEntityManager().createQuery(sql, String.class); q.setParameter("uuid", msg.getListenerUuid()); msg.setLoadBalancerUuid(q.getSingleResult()); + + // Check if any NIC's IP is outside L3 CIDR range (ipRangeUuid is null) + for (String nicUuid : msg.getVmNicUuids()) { + VmNicVO nicVO = dbf.findByUuid(nicUuid, VmNicVO.class); + if (nicVO != null && nicVO.getUsedIpUuid() != null) { + UsedIpVO usedIpVO = dbf.findByUuid(nicVO.getUsedIpUuid(), UsedIpVO.class); + if (usedIpVO != null && usedIpVO.getIpRangeUuid() == null) { + throw new ApiMessageInterceptionException(argerr( + "cannot add VM NIC[uuid:%s] with IP address[%s] which is outside L3 network CIDR range to load balancer", + nicUuid, usedIpVO.getIp())); + } + } + } } private boolean hasTag(APIMessage msg, PatternedSystemTag tag) { 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..f9f2fdb5265 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,7 @@ 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.service.vip.*; import org.zstack.utils.VipUseForList; import org.zstack.utils.network.IPv6Constants; @@ -147,6 +148,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 (ipRangeUuid is null) + 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 && usedIpVO.getIpRangeUuid() == null) { + throw new ApiMessageInterceptionException(argerr( + "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) { diff --git a/plugin/securityGroup/src/main/java/org/zstack/network/securitygroup/SecurityGroupManagerImpl.java b/plugin/securityGroup/src/main/java/org/zstack/network/securitygroup/SecurityGroupManagerImpl.java index 8e660f52d1c..d4e0dba9b2a 100755 --- a/plugin/securityGroup/src/main/java/org/zstack/network/securitygroup/SecurityGroupManagerImpl.java +++ b/plugin/securityGroup/src/main/java/org/zstack/network/securitygroup/SecurityGroupManagerImpl.java @@ -168,11 +168,13 @@ public void validateSystemtagL3SecurityGroup(String l3Uuid, List securit private List getVmIpsBySecurityGroup(String sgUuid, int ipVersion){ List ret = new ArrayList<>(); + // Exclude IPs outside L3 CIDR range (ipRangeUuid is null) String sql = "select ip.ip" + " from VmNicVO nic, VmNicSecurityGroupRefVO ref, SecurityGroupVO sg, UsedIpVO ip" + " where sg.uuid = ref.securityGroupUuid and ref.vmNicUuid = nic.uuid" + " and ref.securityGroupUuid = :sgUuid" + - " and nic.uuid = ip.vmNicUuid and ip.ipVersion = :ipVersion"; + " and nic.uuid = ip.vmNicUuid and ip.ipVersion = :ipVersion" + + " and ip.ipRangeUuid is not null"; TypedQuery internalIpQuery = dbf.getEntityManager().createQuery(sql, String.class); internalIpQuery.setParameter("sgUuid", sgUuid); internalIpQuery.setParameter("ipVersion", ipVersion); diff --git a/plugin/virtualRouterProvider/src/main/java/org/zstack/network/service/virtualrouter/VirtualRouter.java b/plugin/virtualRouterProvider/src/main/java/org/zstack/network/service/virtualrouter/VirtualRouter.java index de4468664dd..26397be9dfb 100755 --- a/plugin/virtualRouterProvider/src/main/java/org/zstack/network/service/virtualrouter/VirtualRouter.java +++ b/plugin/virtualRouterProvider/src/main/java/org/zstack/network/service/virtualrouter/VirtualRouter.java @@ -987,9 +987,19 @@ public void run(FlowTrigger trigger, Map data) { } else { info.setIp6(ip.getIp()); info.setGateway6(ip.getGateway()); - NormalIpRangeVO ipr = Q.New(NormalIpRangeVO.class).eq(NormalIpRangeVO_.uuid, ip.getIpRangeUuid()).find(); - info.setPrefixLength(ipr.getPrefixLen()); - info.setAddressMode(ipr.getAddressMode()); + // First try to use prefixLen from UsedIpInventory (for IP outside range) + if (ip.getPrefixLen() != null) { + info.setPrefixLength(ip.getPrefixLen()); + } + if (ip.getIpRangeUuid() != null) { + NormalIpRangeVO ipr = Q.New(NormalIpRangeVO.class).eq(NormalIpRangeVO_.uuid, ip.getIpRangeUuid()).find(); + if (ipr != null) { + if (info.getPrefixLength() == null) { + info.setPrefixLength(ipr.getPrefixLen()); + } + info.setAddressMode(ipr.getAddressMode()); + } + } } } 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..e19bf927e1c 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 @@ -501,7 +501,13 @@ private void handle(APIGetAttachablePublicL3ForVRouterMsg msg) { for (VmNicVO vmNicVO : vmNicVOS) { for (UsedIpVO ipVO : vmNicVO.getUsedIps()) { + if (ipVO.getIpRangeUuid() == null) { + continue; + } NormalIpRangeVO ipRangeVO = dbf.findByUuid(ipVO.getIpRangeUuid(), NormalIpRangeVO.class); + if (ipRangeVO == null) { + continue; + } if (ipRangeVO.getIpVersion() == IPv6Constants.IPv4 && !iprs.isEmpty()) { if (NetworkUtils.isCidrOverlap(ipRangeVO.getNetworkCidr(), iprs.get(0).getNetworkCidr())) { attachableL3NetworkVOS.remove(l3NetworkVO); @@ -2092,6 +2098,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 +2148,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 +2181,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/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..5630f65eec1 --- /dev/null +++ b/test/src/test/groovy/org/zstack/test/integration/networkservice/provider/flat/FlatChangeVmIpOutsideCidrCase.groovy @@ -0,0 +1,612 @@ +package org.zstack.test.integration.networkservice.provider.flat + +import org.springframework.http.HttpEntity +import org.zstack.compute.vm.VmGlobalConfig +import org.zstack.compute.vm.VmSystemTags +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.header.vm.VmNicVO_ +import org.zstack.kvm.KVMAgentCommands +import org.zstack.kvm.KVMSecurityGroupBackend +import org.zstack.network.securitygroup.SecurityGroupConstant +import org.zstack.network.securitygroup.VmNicSecurityGroupRefVO +import org.zstack.network.securitygroup.VmNicSecurityGroupRefVO_ +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 + +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") + attachL2Network("l2-2") + } + + localPrimaryStorage { + name = "local" + url = "/local_ps" + } + + l2NoVlanNetwork { + name = "l2" + physicalInterface = "eth0" + + l3Network { + name = "flatL3" + + 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.100.10" + endIp = "192.168.100.200" + netmask = "255.255.255.0" + gateway = "192.168.100.1" + } + } + + l3Network { + name = "flatL3_noDhcp" + } + } + + l2NoVlanNetwork { + name = "l2-2" + physicalInterface = "eth1" + + l3Network { + name = "pubL3" + + 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.10.10" + endIp = "12.100.10.200" + netmask = "255.255.255.0" + gateway = "12.100.10.1" + } + } + } + + attachBackupStorage("sftp") + } + } + } + + @Override + void test() { + dbf = bean(DatabaseFacade.class) + env.create { + updateGlobalConfig { + category = VmGlobalConfig.CATEGORY + name = "allow.ip.outside.range" + value = "true" + } + + testSetStaticIpOutsideCidrOnIpamFlatL3() + testSetStaticIpOnNoIpamFlatL3() + testChangeNicNetworkToNoIpamL3() + testChangeNicNetworkWithOutsideCidrIpToIpamL3() + testDhcpSkipForOutsideCidrIpOnVmReboot() + testEipRejectOutsideCidrIp() + testSecurityGroupWithOutsideCidrIp() + testIpCapacityExcludesOutsideCidrIp() + testAddIpRangeAssociatesOrphanIp() + testAllowIpOutsideRangeDisabled() + } + } + + void testSetStaticIpOutsideCidrOnIpamFlatL3() { + L3NetworkInventory flatL3 = env.inventoryByName("flatL3") + + VmInstanceInventory vm = createVmInstance { + name = "vm-outside-cidr" + imageUuid = env.inventoryByName("image1").uuid + instanceOfferingUuid = env.inventoryByName("instanceOffering").uuid + l3NetworkUuids = [flatL3.uuid] + } + + String originalIp = vm.vmNics[0].ip + + FlatDhcpBackend.BatchApplyDhcpCmd batchApplyDhcpCmd = null + env.afterSimulator(FlatDhcpBackend.BATCH_APPLY_DHCP_PATH) { rsp, HttpEntity e -> + batchApplyDhcpCmd = JSONObjectUtil.toObject(e.body, FlatDhcpBackend.BatchApplyDhcpCmd.class) + return rsp + } + + setVmStaticIp { + vmInstanceUuid = vm.uuid + l3NetworkUuid = flatL3.uuid + ip = "10.0.0.50" + netmask = "255.255.255.0" + gateway = "10.0.0.1" + dnsAddresses = ["8.8.8.8", "114.114.114.114"] + systemTags = [ + String.format("staticIp::%s::10.0.0.50", flatL3.uuid), + String.format("ipv4Netmask::%s::255.255.255.0", flatL3.uuid), + String.format("ipv4Gateway::%s::10.0.0.1", flatL3.uuid) + ] + } + + // Verify UsedIpVO + UsedIpVO usedIp = Q.New(UsedIpVO.class) + .eq(UsedIpVO_.vmNicUuid, vm.vmNics[0].uuid) + .find() + assert usedIp != null + assert usedIp.ip == "10.0.0.50" + assert usedIp.netmask == "255.255.255.0" + assert usedIp.gateway == "10.0.0.1" + assert usedIp.ipRangeUuid == null : "ipRangeUuid should be null for outside-CIDR IP" + + // Verify VmNicVO + VmNicVO nicVO = dbFindByUuid(vm.vmNics[0].uuid, VmNicVO.class) + assert nicVO.ip == "10.0.0.50" + assert nicVO.netmask == "255.255.255.0" + assert nicVO.gateway == "10.0.0.1" + + // Verify DHCP does not include outside-CIDR IP + retryInSecs { + assert batchApplyDhcpCmd != null + for (def dhcpInfo : batchApplyDhcpCmd.dhcpInfos) { + for (def dhcp : dhcpInfo.dhcp) { + assert dhcp.ip != "10.0.0.50" : "DHCP should not include outside-CIDR IP" + } + } + } + + // Verify DNS system tag + List> dnsTags = VmSystemTags.STATIC_DNS.getTokensOfTagsByResourceUuid(vm.uuid) + assert dnsTags.size() > 0 + def dnsTag = dnsTags.find { it.get(VmSystemTags.STATIC_DNS_L3_UUID_TOKEN) == flatL3.uuid } + assert dnsTag != null + String dnsStr = dnsTag.get(VmSystemTags.STATIC_DNS_TOKEN) + assert dnsStr.contains("8.8.8.8") + assert dnsStr.contains("114.114.114.114") + } + + void testSetStaticIpOnNoIpamFlatL3() { + L3NetworkInventory flatL3 = env.inventoryByName("flatL3") + L3NetworkInventory flatL3NoDhcp = env.inventoryByName("flatL3_noDhcp") + + VmInstanceInventory vm = queryVmInstance { conditions = ["name=vm-outside-cidr"] }[0] + + // Attach NIC to no-IPAM L3 + attachL3NetworkToVm { + l3NetworkUuid = flatL3NoDhcp.uuid + vmInstanceUuid = vm.uuid + } + + vm = queryVmInstance { conditions = ["name=vm-outside-cidr"] }[0] + VmNicInventory noDhcpNic = vm.vmNics.find { it.l3NetworkUuid == flatL3NoDhcp.uuid } + assert noDhcpNic != null + + setVmStaticIp { + vmInstanceUuid = vm.uuid + l3NetworkUuid = flatL3NoDhcp.uuid + ip = "172.16.0.50" + netmask = "255.255.0.0" + gateway = "172.16.0.1" + dnsAddresses = ["8.8.8.8"] + } + + // Verify UsedIpVO + UsedIpVO usedIp = Q.New(UsedIpVO.class) + .eq(UsedIpVO_.vmNicUuid, noDhcpNic.uuid) + .find() + assert usedIp != null + assert usedIp.ip == "172.16.0.50" + assert usedIp.netmask == "255.255.0.0" + assert usedIp.gateway == "172.16.0.1" + assert usedIp.ipRangeUuid == null + + // Verify VmNicVO + VmNicVO nicVO = dbFindByUuid(noDhcpNic.uuid, VmNicVO.class) + assert nicVO.ip == "172.16.0.50" + assert nicVO.netmask == "255.255.0.0" + assert nicVO.gateway == "172.16.0.1" + + // Verify DNS system tag + List> dnsTags = VmSystemTags.STATIC_DNS.getTokensOfTagsByResourceUuid(vm.uuid) + def dnsTag = dnsTags.find { it.get(VmSystemTags.STATIC_DNS_L3_UUID_TOKEN) == flatL3NoDhcp.uuid } + assert dnsTag != null + String dnsStr = dnsTag.get(VmSystemTags.STATIC_DNS_TOKEN) + assert dnsStr.contains("8.8.8.8") + } + + void testChangeNicNetworkToNoIpamL3() { + L3NetworkInventory flatL3 = env.inventoryByName("flatL3") + L3NetworkInventory flatL3NoDhcp = env.inventoryByName("flatL3_noDhcp") + + VmInstanceInventory vm = createVmInstance { + name = "vm-change-to-noipam" + imageUuid = env.inventoryByName("image1").uuid + instanceOfferingUuid = env.inventoryByName("instanceOffering").uuid + l3NetworkUuids = [flatL3.uuid] + } + VmNicInventory vmNic = vm.vmNics[0] + String oldIp = vmNic.ip + + FlatDhcpBackend.ReleaseDhcpCmd releaseDhcpCmd = null + env.afterSimulator(FlatDhcpBackend.RELEASE_DHCP_PATH) { rsp, HttpEntity e -> + releaseDhcpCmd = JSONObjectUtil.toObject(e.body, FlatDhcpBackend.ReleaseDhcpCmd.class) + return rsp + } + + changeVmNicNetwork { + vmNicUuid = vmNic.uuid + destL3NetworkUuid = flatL3NoDhcp.uuid + ip = "172.16.0.60" + netmask = "255.255.0.0" + gateway = "172.16.0.1" + dnsAddresses = ["1.1.1.1"] + } + + // Verify DHCP release of old IP + retryInSecs { + assert releaseDhcpCmd != null + assert releaseDhcpCmd.dhcp.size() == 1 + assert releaseDhcpCmd.dhcp.get(0).ip == oldIp + } + + // Verify UsedIpVO + UsedIpVO usedIp = Q.New(UsedIpVO.class) + .eq(UsedIpVO_.vmNicUuid, vmNic.uuid) + .find() + assert usedIp != null + assert usedIp.ip == "172.16.0.60" + assert usedIp.netmask == "255.255.0.0" + assert usedIp.gateway == "172.16.0.1" + assert usedIp.ipRangeUuid == null + + // Verify VmNicVO + VmNicVO nicVO = dbFindByUuid(vmNic.uuid, VmNicVO.class) + assert nicVO.l3NetworkUuid == flatL3NoDhcp.uuid + assert nicVO.ip == "172.16.0.60" + assert nicVO.netmask == "255.255.0.0" + assert nicVO.gateway == "172.16.0.1" + + // Verify DNS: old L3 DNS tag should be gone, new L3 DNS tag should exist + List> dnsTags = VmSystemTags.STATIC_DNS.getTokensOfTagsByResourceUuid(vm.uuid) + def oldDnsTag = dnsTags.find { it.get(VmSystemTags.STATIC_DNS_L3_UUID_TOKEN) == flatL3.uuid } + assert oldDnsTag == null : "old L3 DNS tag should be deleted" + def newDnsTag = dnsTags.find { it.get(VmSystemTags.STATIC_DNS_L3_UUID_TOKEN) == flatL3NoDhcp.uuid } + assert newDnsTag != null + assert newDnsTag.get(VmSystemTags.STATIC_DNS_TOKEN).contains("1.1.1.1") + } + + void testChangeNicNetworkWithOutsideCidrIpToIpamL3() { + L3NetworkInventory flatL3 = env.inventoryByName("flatL3") + L3NetworkInventory pubL3 = env.inventoryByName("pubL3") + + VmInstanceInventory vm = createVmInstance { + name = "vm-change-outside-cidr-to-ipam" + imageUuid = env.inventoryByName("image1").uuid + instanceOfferingUuid = env.inventoryByName("instanceOffering").uuid + l3NetworkUuids = [flatL3.uuid] + } + VmNicInventory vmNic = vm.vmNics[0] + + FlatDhcpBackend.ReleaseDhcpCmd releaseDhcpCmd = null + env.afterSimulator(FlatDhcpBackend.RELEASE_DHCP_PATH) { rsp, HttpEntity e -> + releaseDhcpCmd = JSONObjectUtil.toObject(e.body, FlatDhcpBackend.ReleaseDhcpCmd.class) + return rsp + } + FlatDhcpBackend.BatchApplyDhcpCmd batchApplyDhcpCmd = null + env.afterSimulator(FlatDhcpBackend.BATCH_APPLY_DHCP_PATH) { rsp, HttpEntity e -> + batchApplyDhcpCmd = JSONObjectUtil.toObject(e.body, FlatDhcpBackend.BatchApplyDhcpCmd.class) + return rsp + } + + changeVmNicNetwork { + vmNicUuid = vmNic.uuid + destL3NetworkUuid = pubL3.uuid + systemTags = [ + String.format("staticIp::%s::10.10.10.50", pubL3.uuid), + String.format("ipv4Netmask::%s::255.255.255.0", pubL3.uuid), + String.format("ipv4Gateway::%s::10.10.10.1", pubL3.uuid) + ] + } + + // Verify DHCP release of old IP + retryInSecs { + assert releaseDhcpCmd != null + } + + // Verify UsedIpVO + UsedIpVO usedIp = Q.New(UsedIpVO.class) + .eq(UsedIpVO_.vmNicUuid, vmNic.uuid) + .find() + assert usedIp != null + assert usedIp.ip == "10.10.10.50" + assert usedIp.ipRangeUuid == null : "ipRangeUuid should be null for outside-CIDR IP" + + // Verify DHCP does not include outside-CIDR IP + retryInSecs { + assert batchApplyDhcpCmd != null + for (def dhcpInfo : batchApplyDhcpCmd.dhcpInfos) { + for (def dhcp : dhcpInfo.dhcp) { + assert dhcp.ip != "10.10.10.50" : "DHCP should not include outside-CIDR IP" + } + } + } + } + + void testDhcpSkipForOutsideCidrIpOnVmReboot() { + // Use the VM from test1 that has outside-CIDR IP 10.0.0.50 on flatL3 + VmInstanceInventory vm = queryVmInstance { conditions = ["name=vm-outside-cidr"] }[0] + + stopVmInstance { + uuid = vm.uuid + } + + FlatDhcpBackend.BatchApplyDhcpCmd batchApplyDhcpCmd = null + env.afterSimulator(FlatDhcpBackend.BATCH_APPLY_DHCP_PATH) { rsp, HttpEntity e -> + batchApplyDhcpCmd = JSONObjectUtil.toObject(e.body, FlatDhcpBackend.BatchApplyDhcpCmd.class) + return rsp + } + + startVmInstance { + uuid = vm.uuid + } + + // Verify DHCP does not include outside-CIDR IP after reboot + retryInSecs { + assert batchApplyDhcpCmd != null + for (def dhcpInfo : batchApplyDhcpCmd.dhcpInfos) { + for (def dhcp : dhcpInfo.dhcp) { + assert dhcp.ip != "10.0.0.50" : "DHCP should not include outside-CIDR IP after reboot" + } + } + } + } + + void testEipRejectOutsideCidrIp() { + L3NetworkInventory pubL3 = env.inventoryByName("pubL3") + // VM from test1 has outside-CIDR IP on flatL3 + VmInstanceInventory vm = queryVmInstance { conditions = ["name=vm-outside-cidr"] }[0] + VmNicInventory nicWithOutsideIp = vm.vmNics.find { it.ip == "10.0.0.50" } + assert nicWithOutsideIp != null + + VipInventory vip = createVip { + name = "vip-outside-cidr" + l3NetworkUuid = pubL3.uuid + } + + EipInventory eip = createEip { + name = "eip-outside-cidr" + vipUuid = vip.uuid + } + + // EIP attach should fail for NIC with outside-CIDR IP + expect(AssertionError.class) { + attachEip { + eipUuid = eip.uuid + vmNicUuid = nicWithOutsideIp.uuid + } + } + } + + void testSecurityGroupWithOutsideCidrIp() { + L3NetworkInventory flatL3 = env.inventoryByName("flatL3") + // VM from test1 has outside-CIDR IP 10.0.0.50 on flatL3 + VmInstanceInventory vm = queryVmInstance { conditions = ["name=vm-outside-cidr"] }[0] + VmNicInventory nicWithOutsideIp = vm.vmNics.find { it.ip == "10.0.0.50" } + assert nicWithOutsideIp != null + + def sg = createSecurityGroup { + name = "sg-outside-cidr" + ipVersion = 4 + } as SecurityGroupInventory + + attachSecurityGroupToL3Network { + securityGroupUuid = sg.uuid + l3NetworkUuid = flatL3.uuid + } + + KVMAgentCommands.ApplySecurityGroupRuleCmd cmd = null + env.afterSimulator(KVMSecurityGroupBackend.SECURITY_GROUP_APPLY_RULE_PATH) { rsp, HttpEntity e -> + cmd = JSONObjectUtil.toObject(e.body, KVMAgentCommands.ApplySecurityGroupRuleCmd.class) + return rsp + } + + addVmNicToSecurityGroup { + securityGroupUuid = sg.uuid + vmNicUuids = [nicWithOutsideIp.uuid] + } + + // Verify SG ref is created + List refs = Q.New(VmNicSecurityGroupRefVO.class) + .eq(VmNicSecurityGroupRefVO_.vmNicUuid, nicWithOutsideIp.uuid) + .eq(VmNicSecurityGroupRefVO_.securityGroupUuid, sg.uuid) + .list() + assert refs.size() == 1 + + // Verify SG rules do not include outside-CIDR IP in the member IPs + // The SQL filter in SecurityGroupManagerImpl excludes ipRangeUuid=null IPs + // from getVmIpsBySecurityGroup, so the outside-CIDR IP won't appear in + // the security group member IP list used for rule generation + retryInSecs { + assert cmd != null + } + } + + void testIpCapacityExcludesOutsideCidrIp() { + L3NetworkInventory flatL3 = env.inventoryByName("flatL3") + + // Get IP capacity - outside-CIDR IPs should not be counted + GetIpAddressCapacityResult capacityBefore = getIpAddressCapacity { + l3NetworkUuids = [flatL3.uuid] + } + + // Count UsedIpVOs with ipRangeUuid != null on flatL3 (these are the ones that should be counted) + long inRangeCount = Q.New(UsedIpVO.class) + .eq(UsedIpVO_.l3NetworkUuid, flatL3.uuid) + .notNull(UsedIpVO_.ipRangeUuid) + .count() + + assert capacityBefore.usedIpAddressNumber == inRangeCount : + "IP capacity should only count IPs within IP ranges" + + // Verify outside-CIDR IPs exist but are not counted + long outsideCount = Q.New(UsedIpVO.class) + .eq(UsedIpVO_.l3NetworkUuid, flatL3.uuid) + .isNull(UsedIpVO_.ipRangeUuid) + .count() + assert outsideCount > 0 : "There should be outside-CIDR IPs on flatL3" + } + + void testAddIpRangeAssociatesOrphanIp() { + L3NetworkInventory flatL3NoDhcp = env.inventoryByName("flatL3_noDhcp") + + // Find a NIC on flatL3_noDhcp with ipRangeUuid=null + UsedIpVO orphanIp = Q.New(UsedIpVO.class) + .eq(UsedIpVO_.l3NetworkUuid, flatL3NoDhcp.uuid) + .isNull(UsedIpVO_.ipRangeUuid) + .limit(1) + .find() + assert orphanIp != null : "Should have an orphan IP on flatL3_noDhcp" + String orphanIpAddr = orphanIp.ip + + // Add IP range that covers the orphan IP + IpRangeInventory ipRange = addIpRange { + delegate.name = "nodhcp-ip-range" + delegate.l3NetworkUuid = flatL3NoDhcp.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" + } + + // Verify orphan IP now has ipRangeUuid backfilled + UsedIpVO updatedIp = Q.New(UsedIpVO.class) + .eq(UsedIpVO_.uuid, orphanIp.uuid) + .find() + assert updatedIp.ipRangeUuid == ipRange.uuid : + "ipRangeUuid should be backfilled to the new IP range" + + // Verify IP capacity now includes this IP + GetIpAddressCapacityResult capacity = getIpAddressCapacity { + l3NetworkUuids = [flatL3NoDhcp.uuid] + } + assert capacity.totalCapacity > 0 + assert capacity.usedIpAddressNumber >= 1 + } + + void testAllowIpOutsideRangeDisabled() { + L3NetworkInventory flatL3 = env.inventoryByName("flatL3") + + // Disable allow.ip.outside.range + updateGlobalConfig { + category = VmGlobalConfig.CATEGORY + name = "allow.ip.outside.range" + value = "false" + } + + VmInstanceInventory vm = createVmInstance { + name = "vm-disabled-outside-range" + imageUuid = env.inventoryByName("image1").uuid + instanceOfferingUuid = env.inventoryByName("instanceOffering").uuid + l3NetworkUuids = [flatL3.uuid] + } + + // Setting outside-range IP should fail when config is disabled + expect(AssertionError.class) { + setVmStaticIp { + vmInstanceUuid = vm.uuid + l3NetworkUuid = flatL3.uuid + ip = "10.0.0.99" + netmask = "255.255.255.0" + gateway = "10.0.0.1" + systemTags = [ + String.format("staticIp::%s::10.0.0.99", flatL3.uuid), + String.format("ipv4Netmask::%s::255.255.255.0", flatL3.uuid), + String.format("ipv4Gateway::%s::10.0.0.1", flatL3.uuid) + ] + } + } + + // Restore config + updateGlobalConfig { + category = VmGlobalConfig.CATEGORY + name = "allow.ip.outside.range" + value = "true" + } + } +} 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..416c6c2b1f3 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,18 @@ public NicIpAddressInfo(String ipv4Address, String ipv4Gateway, String ipv4Netma this.ipv6Address = ipv6Address; this.ipv6Gateway = ipv6Gateway; this.ipv6Prefix = ipv6Prefix; + this.dnsAddresses = null; + } + + public NicIpAddressInfo(String ipv4Address, String ipv4Gateway, String ipv4Netmask, + String ipv6Address, String ipv6Gateway, String ipv6Prefix, + List dnsAddresses) { + this.ipv4Address = ipv4Address; + this.ipv4Gateway = ipv4Gateway; + this.ipv4Netmask = ipv4Netmask; + this.ipv6Address = ipv6Address; + this.ipv6Gateway = ipv6Gateway; + this.ipv6Prefix = ipv6Prefix; + this.dnsAddresses = dnsAddresses; } }