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..b4e76fb6511 100755 --- a/compute/src/main/java/org/zstack/compute/vm/StaticIpOperator.java +++ b/compute/src/main/java/org/zstack/compute/vm/StaticIpOperator.java @@ -14,6 +14,7 @@ import org.zstack.header.message.APIMessage; import org.zstack.header.message.MessageReply; import org.zstack.header.network.l3.*; +import org.zstack.network.l3.L3NetworkSystemTags; import org.zstack.header.tag.SystemTagCreateMessageValidator; import org.zstack.header.tag.SystemTagVO; import org.zstack.header.tag.SystemTagVO_; @@ -30,6 +31,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; @@ -134,6 +136,17 @@ public Map getNicNetworkInfoBySystemTag(List s } ret.get(l3Uuid).ipv6Prefix = token.get(VmSystemTags.IPV6_PREFIX_TOKEN); } + if(L3NetworkSystemTags.STATIC_DNS.isMatch(sysTag)) { + Map token = TagUtils.parse(L3NetworkSystemTags.STATIC_DNS.getTagFormat(), sysTag); + String l3Uuid = token.get(L3NetworkSystemTags.STATIC_DNS_L3_UUID_TOKEN); + if (ret.get(l3Uuid) == null) { + continue; + } + String dnsStr = token.get(L3NetworkSystemTags.STATIC_DNS_TOKEN); + if (dnsStr != null && !dnsStr.isEmpty()) { + ret.get(l3Uuid).dnsAddresses = Arrays.asList(dnsStr.split(",")); + } + } } return ret; @@ -222,6 +235,66 @@ 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)); + } + } + + String dnsStr = String.join(",", dnsAddresses); + + 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(L3NetworkSystemTags.STATIC_DNS.instantiateTag( + map(e(L3NetworkSystemTags.STATIC_DNS_L3_UUID_TOKEN, l3Uuid)) + ))); + String tagUuid = q.findValue(); + + if (tagUuid == null) { + SystemTagCreator creator = L3NetworkSystemTags.STATIC_DNS.newSystemTagCreator(vmUuid); + creator.setTagByTokens(map( + e(L3NetworkSystemTags.STATIC_DNS_L3_UUID_TOKEN, l3Uuid), + e(L3NetworkSystemTags.STATIC_DNS_TOKEN, dnsStr) + )); + creator.create(); + } else { + L3NetworkSystemTags.STATIC_DNS.updateByTagUuid(tagUuid, L3NetworkSystemTags.STATIC_DNS.instantiateTag(map( + e(L3NetworkSystemTags.STATIC_DNS_L3_UUID_TOKEN, l3Uuid), + e(L3NetworkSystemTags.STATIC_DNS_TOKEN, dnsStr) + ))); + } + } + + public void deleteStaticDnsByVmUuidAndL3Uuid(String vmUuid, String l3Uuid) { + L3NetworkSystemTags.STATIC_DNS.delete(vmUuid, TagUtils.tagPatternToSqlPattern(L3NetworkSystemTags.STATIC_DNS.instantiateTag( + map(e(L3NetworkSystemTags.STATIC_DNS_L3_UUID_TOKEN, l3Uuid)) + ))); + } + + public List getStaticDnsByVmUuidAndL3Uuid(String vmUuid, String l3Uuid) { + List> tokenList = L3NetworkSystemTags.STATIC_DNS.getTokensOfTagsByResourceUuid(vmUuid); + for (Map tokens : tokenList) { + String uuid = tokens.get(L3NetworkSystemTags.STATIC_DNS_L3_UUID_TOKEN); + if (uuid.equals(l3Uuid)) { + String dnsStr = tokens.get(L3NetworkSystemTags.STATIC_DNS_TOKEN); + if (dnsStr != null && !dnsStr.isEmpty()) { + return Arrays.asList(dnsStr.split(",")); + } + } + } + return null; + } + public Map getNicStaticIpMap(List nicStaticIpList) { Map nicStaticIpMap = new HashMap<>(); if (nicStaticIpList != null) { @@ -264,6 +337,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 +399,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(); @@ -338,11 +418,22 @@ public List fillUpStaticIpInfoToVmNics(Map sta .eq(NormalIpRangeVO_.l3NetworkUuid, l3Uuid) .eq(NormalIpRangeVO_.ipVersion, IPv6Constants.IPv4) .limit(1).find(); - if (ipRangeVO == null) { + + // Check if IP is within the range + boolean ipInRange = ipRangeVO != null && + NetworkUtils.isInRange(nicIp.ipv4Address, ipRangeVO.getStartIp(), ipRangeVO.getEndIp()); + + if (ipRangeVO == null || (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), @@ -370,11 +461,22 @@ public List fillUpStaticIpInfoToVmNics(Map sta .eq(NormalIpRangeVO_.l3NetworkUuid, l3Uuid) .eq(NormalIpRangeVO_.ipVersion, IPv6Constants.IPv6) .limit(1).find(); - if (ipRangeVO == null) { + + // Check if IPv6 is within the range + boolean ipInRange = ipRangeVO != null && + IPv6NetworkUtils.isIpv6InRange(nicIp.ipv6Address, ipRangeVO.getStartIp(), ipRangeVO.getEndIp()); + + if (ipRangeVO == null || (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), diff --git a/compute/src/main/java/org/zstack/compute/vm/VmGlobalConfig.java b/compute/src/main/java/org/zstack/compute/vm/VmGlobalConfig.java index dc39bce94fe..69809d5b79e 100755 --- a/compute/src/main/java/org/zstack/compute/vm/VmGlobalConfig.java +++ b/compute/src/main/java/org/zstack/compute/vm/VmGlobalConfig.java @@ -122,6 +122,10 @@ public class VmGlobalConfig { @GlobalConfigValidation(validValues = {"true", "false"}) public static GlobalConfig ENABLE_VM_INTERNAL_IP_OVERWRITE = new GlobalConfig(CATEGORY, "enable.vm.internal.ip.overwrite"); + @GlobalConfigValidation(validValues = {"true", "false"}) + @GlobalConfigDef(defaultValue = "false", 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"); + @GlobalConfigValidation(validValues = {"true", "false"}) public static GlobalConfig UNIQUE_VM_NAME = new GlobalConfig(CATEGORY, "uniqueVmName"); 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..f61621e6b0b 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,10 @@ public void run(FlowTrigger trigger, Map data) { done(new FlowDoneHandler(completion) { @Override public void handle(Map data) { + // Set DNS addresses if provided + if (msg.getDnsAddresses() != null) { + new StaticIpOperator().setStaticDns(self.getUuid(), msg.getL3NetworkUuid(), msg.getDnsAddresses()); + } completion.success(); } }); @@ -3684,6 +3689,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(); } @@ -6219,6 +6228,7 @@ private void handle(APIChangeVmNicNetworkMsg msg) { cmsg.setVmInstanceUuid(msg.getVmInstanceUuid()); cmsg.setRequiredIpMap(msg.getRequiredIpMap()); cmsg.setSystemTags(msg.getSystemTags()); + cmsg.setDnsAddresses(msg.getDnsAddresses()); bus.makeTargetServiceIdByResourceUuid(cmsg, VmInstanceConstant.SERVICE_ID, cmsg.getVmInstanceUuid()); bus.send(cmsg, new CloudBusCallBack(msg) { @Override @@ -6246,6 +6256,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 +6277,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() { 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..be492f3b18d 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 + false + + 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..b1cae19f925 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,9 @@ public class APIChangeVmNicNetworkMsg extends APIMessage implements VmInstanceMe private String staticIp; + @APIParam(required = false) + private List dnsAddresses; + public String getVmNicUuid() { return vmNicUuid; } @@ -80,4 +83,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..0d019b13968 100644 --- a/header/src/main/java/org/zstack/header/vm/ChangeVmNicNetworkMsg.java +++ b/header/src/main/java/org/zstack/header/vm/ChangeVmNicNetworkMsg.java @@ -14,6 +14,7 @@ public class ChangeVmNicNetworkMsg extends NeedReplyMessage implements VmInstanc private String vmInstanceUuid; private Map> requiredIpMap; private String staticIp; + private List dnsAddresses; public String getVmNicUuid() { return vmNicUuid; @@ -55,4 +56,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/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/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..674f63c2fc9 --- /dev/null +++ b/network/src/main/java/org/zstack/network/l3/CHANGES-VmNicIpOutsideCidr.md @@ -0,0 +1,259 @@ +# 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 List dnsAddresses; +``` + +**文件**: `header/src/main/java/org/zstack/header/vm/ChangeVmNicNetworkMsg.java` +```java +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 = "false", type = Boolean.class, + description = "Allow VM NIC to use IP address outside of L3 network IP ranges") +public static GlobalConfig ALLOW_IP_OUTSIDE_RANGE = new GlobalConfig(CATEGORY, "allow.ip.outside.range"); +``` + +**配置项**: `vm.allow.ip.outside.range` +- 默认值: `false` +- 设置为`true`时允许设置CIDR范围外的IP地址 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..b642fcfe32f 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,58 @@ 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) { + try { + UsedIpVO vo = new UsedIpVO(); + String uuid = l3NetworkUuid + ip; + 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(ip); + vo.setNetmask(netmask); + vo.setIpInLong(NetworkUtils.ipv4StringToLong(ip)); + vo.setIpInBinary(NetworkUtils.ipStringToBytes(ip)); + } else { + vo.setIp(IPv6NetworkUtils.getIpv6AddressCanonicalString(ip)); + vo.setNetmask(netmask); + vo.setPrefixLen(prefixLen); + vo.setIpInBinary(NetworkUtils.ipStringToBytes(vo.getIp())); + } + + 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..54a763165d9 100644 --- a/network/src/main/java/org/zstack/network/l3/L3NetworkSystemTags.java +++ b/network/src/main/java/org/zstack/network/l3/L3NetworkSystemTags.java @@ -3,6 +3,7 @@ import org.zstack.header.network.l2.L2NetworkVO; import org.zstack.header.network.l3.L3NetworkVO; import org.zstack.header.tag.TagDefinition; +import org.zstack.header.vm.VmInstanceVO; import org.zstack.tag.PatternedSystemTag; @TagDefinition @@ -32,4 +33,9 @@ public class L3NetworkSystemTags { public static String L3_NETWORK_HUAWEI_LOGICAL_ROUTER_TOKEN = "logicalRouterUuid"; public static PatternedSystemTag L3_NETWORK_HUAWEI_LOGICAL_ROUTER = new PatternedSystemTag(String.format("logicalRouterUuid::{%s}", L3_NETWORK_HUAWEI_LOGICAL_ROUTER_TOKEN), L3NetworkVO.class); + + // 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); } 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..fe7ada3e7f3 100755 --- a/network/src/main/java/org/zstack/network/service/NetworkServiceManagerImpl.java +++ b/network/src/main/java/org/zstack/network/service/NetworkServiceManagerImpl.java @@ -25,6 +25,7 @@ import org.zstack.header.network.service.*; import org.zstack.header.network.service.NetworkServiceExtensionPoint.NetworkServiceExtensionPosition; import org.zstack.header.vm.*; +import org.zstack.network.l3.L3NetworkSystemTags; import org.zstack.query.QueryFacade; import org.zstack.utils.Utils; import org.zstack.utils.logging.CLogger; @@ -483,6 +484,23 @@ 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 = L3NetworkSystemTags.STATIC_DNS.getTokensOfTagsByResourceUuid(vmUuid); + for (Map tokens : tokenList) { + String uuid = tokens.get(L3NetworkSystemTags.STATIC_DNS_L3_UUID_TOKEN); + if (uuid.equals(l3NetworkUuid)) { + String dnsStr = tokens.get(L3NetworkSystemTags.STATIC_DNS_TOKEN); + if (dnsStr != null && !dnsStr.isEmpty()) { + return Arrays.asList(dnsStr.split(",")); + } + } + } + // 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/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..ce4ac5f0530 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 bindBind EIP to IP address[%s] which is outside L3 network CIDR range", + usedIpVO.getIp())); + } } private void validate(APIDetachEipMsg msg) { 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..df4dae4dc16 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 bindBind 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/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; } }