From 1200179920449451671dd8a8a8c4940b84a0f97a Mon Sep 17 00:00:00 2001 From: "shixin.ruan" Date: Thu, 21 May 2026 12:18:32 +0900 Subject: [PATCH 01/53] [host]: support kvm host ipv6 mgmt ip Allow APIAddHost managementIp to accept IPv6 literals and canonicalize them. Format KVM agent URLs with IPv6 brackets and cover add-host validation with KVM IPv6 case. Resolves: ZSTAC-79206 Change-Id: I6cdaabc8c7d7d62161565b5df6a02ecc61f787b2 --- .../compute/host/HostApiInterceptor.java | 20 +++- .../src/main/java/org/zstack/kvm/KVMHost.java | 2 +- .../java/org/zstack/kvm/KVMHostFactory.java | 2 +- .../java/org/zstack/kvm/KVMHostUtils.java | 6 ++ .../kvm/host/KvmHostIpv6Case.groovy | 95 +++++++++++++++++++ .../CloudOperationsErrorCode.java | 2 + 6 files changed, 123 insertions(+), 4 deletions(-) create mode 100644 test/src/test/groovy/org/zstack/test/integration/kvm/host/KvmHostIpv6Case.groovy diff --git a/compute/src/main/java/org/zstack/compute/host/HostApiInterceptor.java b/compute/src/main/java/org/zstack/compute/host/HostApiInterceptor.java index 0bbbe166d1c..cbb183ed63c 100755 --- a/compute/src/main/java/org/zstack/compute/host/HostApiInterceptor.java +++ b/compute/src/main/java/org/zstack/compute/host/HostApiInterceptor.java @@ -15,6 +15,7 @@ import org.zstack.header.message.APIMessage; import org.zstack.utils.ShellResult; import org.zstack.utils.ShellUtils; +import org.zstack.utils.network.IPv6NetworkUtils; import org.zstack.utils.network.NetworkUtils; import static org.zstack.core.Platform.argerr; @@ -28,6 +29,9 @@ * To change this template use File | Settings | File Templates. */ public class HostApiInterceptor implements ApiMessageInterceptor { + private static final String INVALID_MANAGEMENT_IP_ERROR = + "managementIp[%s] is not a valid IPv4 address, IPv6 address, or hostname"; + @Autowired private CloudBus bus; @Autowired @@ -121,11 +125,23 @@ private void validate(APIUpdateHostMsg msg) { } private void validate(APIAddHostMsg msg) { - if (!NetworkUtils.isIpv4Address(msg.getManagementIp()) && !NetworkUtils.isHostname(msg.getManagementIp())) { - throw new ApiMessageInterceptionException(argerr(ORG_ZSTACK_COMPUTE_HOST_10113, "managementIp[%s] is neither an IPv4 address nor a valid hostname", msg.getManagementIp())); + if (!isValidManagementEndpoint(msg.getManagementIp())) { + throw new ApiMessageInterceptionException(argerr(ORG_ZSTACK_COMPUTE_HOST_10128, INVALID_MANAGEMENT_IP_ERROR, msg.getManagementIp())); + } + + if (IPv6NetworkUtils.isIpv6Address(msg.getManagementIp())) { + msg.setManagementIp(IPv6NetworkUtils.getIpv6AddressCanonicalString(msg.getManagementIp())); } } + private boolean isValidManagementEndpoint(String endpoint) { + if (NetworkUtils.isIpv4Address(endpoint) || NetworkUtils.isHostname(endpoint)) { + return true; + } + + return IPv6NetworkUtils.isIpv6Address(endpoint) && !IPv6NetworkUtils.isLinkLocalAddress(endpoint); + } + private void validate(APIChangeHostStateMsg msg){ HostStatus hostStatus = Q.New(HostVO.class) .select(HostVO_.status) diff --git a/plugin/kvm/src/main/java/org/zstack/kvm/KVMHost.java b/plugin/kvm/src/main/java/org/zstack/kvm/KVMHost.java index b485c343d18..fb9c12d1c73 100755 --- a/plugin/kvm/src/main/java/org/zstack/kvm/KVMHost.java +++ b/plugin/kvm/src/main/java/org/zstack/kvm/KVMHost.java @@ -2711,7 +2711,7 @@ public void done() { private String buildUrl(String path) { UriComponentsBuilder ub = UriComponentsBuilder.newInstance(); ub.scheme(KVMGlobalProperty.AGENT_URL_SCHEME); - ub.host(self.getManagementIp()); + ub.host(KVMHostUtils.formatHostForUrl(self.getManagementIp())); ub.port(KVMGlobalProperty.AGENT_PORT); if (!"".equals(KVMGlobalProperty.AGENT_URL_ROOT_PATH)) { ub.path(KVMGlobalProperty.AGENT_URL_ROOT_PATH); diff --git a/plugin/kvm/src/main/java/org/zstack/kvm/KVMHostFactory.java b/plugin/kvm/src/main/java/org/zstack/kvm/KVMHostFactory.java index 0188c920a83..cb58a569636 100755 --- a/plugin/kvm/src/main/java/org/zstack/kvm/KVMHostFactory.java +++ b/plugin/kvm/src/main/java/org/zstack/kvm/KVMHostFactory.java @@ -1070,7 +1070,7 @@ public List getConnectExtensions() { public KVMHostContext createHostContext(KVMHostVO vo) { UriComponentsBuilder ub = UriComponentsBuilder.newInstance(); ub.scheme(KVMGlobalProperty.AGENT_URL_SCHEME); - ub.host(vo.getManagementIp()); + ub.host(KVMHostUtils.formatHostForUrl(vo.getManagementIp())); ub.port(KVMGlobalProperty.AGENT_PORT); if (!"".equals(KVMGlobalProperty.AGENT_URL_ROOT_PATH)) { ub.path(KVMGlobalProperty.AGENT_URL_ROOT_PATH); diff --git a/plugin/kvm/src/main/java/org/zstack/kvm/KVMHostUtils.java b/plugin/kvm/src/main/java/org/zstack/kvm/KVMHostUtils.java index 3a0d857ff47..54a3ed0bcce 100644 --- a/plugin/kvm/src/main/java/org/zstack/kvm/KVMHostUtils.java +++ b/plugin/kvm/src/main/java/org/zstack/kvm/KVMHostUtils.java @@ -12,6 +12,7 @@ import org.zstack.utils.TagUtils; import org.zstack.utils.logging.CLogger; import org.zstack.utils.logging.CLoggerImpl; +import org.zstack.utils.network.IPv6NetworkUtils; import org.zstack.utils.ssh.SshResult; import org.zstack.utils.ssh.SshShell; @@ -26,6 +27,7 @@ */ public class KVMHostUtils { private static final CLogger logger = CLoggerImpl.getLogger(KVMHostUtils.class); + private static final String URL_IPV6_HOST_FORMAT = "[%s]"; // ZSTAC-84446: br_conn_all_ns is host-internal; exclude from TLS cert SAN // to keep check-flow and deploy-flow IP lists identical. @@ -134,6 +136,10 @@ public static String collectHostIps(String hostUuid, String managementIp, return collectHostIps(newSsh(managementIp, username, password, sshPort), hostUuid, managementIp); } + public static String formatHostForUrl(String host) { + return IPv6NetworkUtils.isIpv6Address(host) ? String.format(URL_IPV6_HOST_FORMAT, host) : host; + } + // ZSTAC-84446: force ansible re-run + libvirtd restart only when operator opted in // or it's a fresh add; skip on plain reconnect to keep kvmagent PID stable. public static boolean shouldForceTlsRedeploy(boolean needDeployTlsCert, diff --git a/test/src/test/groovy/org/zstack/test/integration/kvm/host/KvmHostIpv6Case.groovy b/test/src/test/groovy/org/zstack/test/integration/kvm/host/KvmHostIpv6Case.groovy new file mode 100644 index 00000000000..b2428569378 --- /dev/null +++ b/test/src/test/groovy/org/zstack/test/integration/kvm/host/KvmHostIpv6Case.groovy @@ -0,0 +1,95 @@ +package org.zstack.test.integration.kvm.host + +import org.zstack.core.Platform +import org.zstack.core.db.Q +import org.zstack.header.errorcode.SysErrors +import org.zstack.header.host.HostVO +import org.zstack.header.host.HostVO_ +import org.zstack.sdk.AddKVMHostAction +import org.zstack.sdk.ClusterInventory +import org.zstack.sdk.KVMHostInventory +import org.zstack.test.integration.kvm.KvmTest +import org.zstack.testlib.EnvSpec +import org.zstack.testlib.SubCase + +class KvmHostIpv6Case extends SubCase { + EnvSpec env + ClusterInventory cluster + + private static final String LOOPBACK_IPV6_FULL = "0:0:0:0:0:0:0:1" + private static final String LOOPBACK_IPV6_CANONICAL = "::1" + private static final String LINK_LOCAL_IPV6 = "fe80::1" + private static final String INVALID_MANAGEMENT_IP = "not-an-ip!!" + + @Override + void setup() { + useSpring(KvmTest.springSpec) + } + + @Override + void environment() { + env = HostEnv.noHostBasicEnv() + } + + @Override + void clean() { + env.delete() + } + + @Override + void test() { + env.create { + cluster = env.inventoryByName("cluster") as ClusterInventory + testAddHostWithIpv6() + testRejectInvalidAndLinkLocalIpv6() + } + } + + void testAddHostWithIpv6() { + def action = new AddKVMHostAction() + action.sessionId = adminSession() + action.resourceUuid = Platform.uuid + action.clusterUuid = cluster.uuid + action.managementIp = LOOPBACK_IPV6_FULL + action.name = "kvm-ipv6" + action.username = "root" + action.password = "password" + def res = action.call() + + assert res.error == null + assert (res.value.inventory as KVMHostInventory).managementIp == LOOPBACK_IPV6_CANONICAL + assert Q.New(HostVO.class).eq(HostVO_.managementIp, LOOPBACK_IPV6_CANONICAL).isExists() + } + + void testRejectInvalidAndLinkLocalIpv6() { + long before = Q.New(HostVO.class).count() + + def action = new AddKVMHostAction() + action.sessionId = adminSession() + action.resourceUuid = Platform.uuid + action.clusterUuid = cluster.uuid + action.managementIp = INVALID_MANAGEMENT_IP + action.name = "kvm-invalid" + action.username = "root" + action.password = "password" + def res = action.call() + + assert res.error != null + assert res.error.code == SysErrors.INVALID_ARGUMENT_ERROR.toString() + assert Q.New(HostVO.class).count() == before + + action = new AddKVMHostAction() + action.sessionId = adminSession() + action.resourceUuid = Platform.uuid + action.clusterUuid = cluster.uuid + action.managementIp = LINK_LOCAL_IPV6 + action.name = "kvm-link-local" + action.username = "root" + action.password = "password" + res = action.call() + + assert res.error != null + assert res.error.code == SysErrors.INVALID_ARGUMENT_ERROR.toString() + assert Q.New(HostVO.class).count() == before + } +} diff --git a/utils/src/main/java/org/zstack/utils/clouderrorcode/CloudOperationsErrorCode.java b/utils/src/main/java/org/zstack/utils/clouderrorcode/CloudOperationsErrorCode.java index c38599da944..0956a78d712 100644 --- a/utils/src/main/java/org/zstack/utils/clouderrorcode/CloudOperationsErrorCode.java +++ b/utils/src/main/java/org/zstack/utils/clouderrorcode/CloudOperationsErrorCode.java @@ -11165,6 +11165,8 @@ public class CloudOperationsErrorCode { public static final String ORG_ZSTACK_COMPUTE_HOST_10127 = "ORG_ZSTACK_COMPUTE_HOST_10127"; + public static final String ORG_ZSTACK_COMPUTE_HOST_10128 = "ORG_ZSTACK_COMPUTE_HOST_10128"; + public static final String ORG_ZSTACK_MONITORING_TRIGGER_EXPRESSION_10000 = "ORG_ZSTACK_MONITORING_TRIGGER_EXPRESSION_10000"; public static final String ORG_ZSTACK_MONITORING_TRIGGER_EXPRESSION_10001 = "ORG_ZSTACK_MONITORING_TRIGGER_EXPRESSION_10001"; From 32733c080df9822c769811fe2015ae9528736452 Mon Sep 17 00:00:00 2001 From: "shixin.ruan" Date: Thu, 21 May 2026 13:37:40 +0900 Subject: [PATCH 02/53] [mgt-ipv6]: support mgmt network IPv6 Implement waterfall M1/M2 management network IPv6 support. Resolves: ZSTAC-79206 Change-Id: Icfeba77200168f8ff05139824166243a87d4b10d --- .../compute/host/HostApiInterceptor.java | 6 +- .../org/zstack/core/NetworkGlobalConfig.java | 15 ++ .../main/java/org/zstack/core/Platform.java | 169 ++++++++++++++---- .../org/zstack/core/rest/RESTFacadeImpl.java | 9 +- .../appliancevm/ApplianceVmConstant.java | 1 + .../appliancevm/ApplianceVmFacadeImpl.java | 4 + .../ceph/backup/CephBackupStorageMonBase.java | 3 +- .../ceph/primary/CephPrimaryStorageBase.java | 5 +- .../primary/CephPrimaryStorageMonBase.java | 3 +- .../src/main/java/org/zstack/kvm/KVMHost.java | 6 +- .../java/org/zstack/kvm/KVMHostUtils.java | 3 +- .../primary/nfs/NfsApiParamChecker.java | 41 ++++- .../VxlanPoolApiInterceptor.java | 10 +- .../core/ManagementNetworkIpv6Case.groovy | 98 ++++++++++ .../utils/network/IPv6NetworkUtils.java | 43 +++++ .../zstack/utils/network/NetworkUtils.java | 22 ++- .../org/zstack/utils/zsha2/ZSha2Helper.java | 2 +- 17 files changed, 377 insertions(+), 63 deletions(-) create mode 100644 core/src/main/java/org/zstack/core/NetworkGlobalConfig.java create mode 100644 test/src/test/groovy/org/zstack/test/integration/core/ManagementNetworkIpv6Case.groovy diff --git a/compute/src/main/java/org/zstack/compute/host/HostApiInterceptor.java b/compute/src/main/java/org/zstack/compute/host/HostApiInterceptor.java index cbb183ed63c..0cb297076be 100755 --- a/compute/src/main/java/org/zstack/compute/host/HostApiInterceptor.java +++ b/compute/src/main/java/org/zstack/compute/host/HostApiInterceptor.java @@ -135,11 +135,7 @@ private void validate(APIAddHostMsg msg) { } private boolean isValidManagementEndpoint(String endpoint) { - if (NetworkUtils.isIpv4Address(endpoint) || NetworkUtils.isHostname(endpoint)) { - return true; - } - - return IPv6NetworkUtils.isIpv6Address(endpoint) && !IPv6NetworkUtils.isLinkLocalAddress(endpoint); + return IPv6NetworkUtils.isValidManagementEndpoint(endpoint); } private void validate(APIChangeHostStateMsg msg){ diff --git a/core/src/main/java/org/zstack/core/NetworkGlobalConfig.java b/core/src/main/java/org/zstack/core/NetworkGlobalConfig.java new file mode 100644 index 00000000000..2195788fc47 --- /dev/null +++ b/core/src/main/java/org/zstack/core/NetworkGlobalConfig.java @@ -0,0 +1,15 @@ +package org.zstack.core; + +import org.zstack.core.config.GlobalConfig; +import org.zstack.core.config.GlobalConfigDef; +import org.zstack.core.config.GlobalConfigDefinition; +import org.zstack.core.config.GlobalConfigValidation; + +@GlobalConfigDefinition +public class NetworkGlobalConfig { + public static final String CATEGORY = "managementServer"; + + @GlobalConfigValidation + @GlobalConfigDef(defaultValue = "false", type = Boolean.class, description = "Prefer IPv6 for management server address selection") + public static GlobalConfig PREFER_IPV6 = new GlobalConfig(CATEGORY, "prefer.ipv6"); +} diff --git a/core/src/main/java/org/zstack/core/Platform.java b/core/src/main/java/org/zstack/core/Platform.java index 78b184d12e7..1f27ea79d14 100755 --- a/core/src/main/java/org/zstack/core/Platform.java +++ b/core/src/main/java/org/zstack/core/Platform.java @@ -36,6 +36,7 @@ import org.zstack.utils.data.StringTemplate; import org.zstack.utils.logging.CLogger; import org.zstack.utils.logging.CLoggerImpl; +import org.zstack.utils.network.IPv6Constants; import org.zstack.utils.network.IPv6NetworkUtils; import org.zstack.utils.network.NetworkUtils; import org.zstack.utils.path.PathUtil; @@ -78,6 +79,15 @@ public class Platform { private static String managementServerCidr; private static MessageSource messageSource; private static String encryptionKey = EncryptRSA.generateKeyString("ZStack open source"); + private static final String MANAGEMENT_SERVER_IP_PROPERTY = "management.server.ip"; + private static final String MANAGEMENT_SERVER_PREFER_IPV6_PROPERTY = "management.server.prefer.ipv6"; + private static final String ZSTACK_MANAGEMENT_SERVER_IP_ENV = "ZSTACK_MANAGEMENT_SERVER_IP"; + private static final String IPV4_ADDRESS_COMMAND = "ip -4 add"; + private static final String IPV6_ADDRESS_COMMAND = "ip -6 addr"; + private static final String DEFAULT_ROUTE_COMMAND = "/sbin/ip route"; + private static final String DEFAULT_ROUTE_MARK = "default via"; + private static final String JGROUPS_INITIAL_HOST_FORMAT = "%s[%s],%s[%s]"; + private static final int IP_ADDRESS_COMMAND_CIDR_INDEX = 1; private static EncryptRSA rsa = new EncryptRSA(); private static Map errorCounter = new HashMap<>(); @@ -442,12 +452,10 @@ private static void prepareHibernateSearchProperties() { if (info.getPeerip() == null) { throw new RuntimeException("the ip of peer node was null, please check the config of zsha2"); } - SearchGlobalProperty.JGroupInfinispanInitialHosts = String.format("%s[%s],%s[%s]", - info.getNodeip(), SearchGlobalProperty.JGroupInfinispanPort, - info.getPeerip(), SearchGlobalProperty.JGroupInfinispanPort); - SearchGlobalProperty.JGroupBackendInitialHosts = String.format("%s[%s],%s[%s]", - info.getNodeip(), SearchGlobalProperty.JGroupBackendPort, - info.getPeerip(), SearchGlobalProperty.JGroupBackendPort); + SearchGlobalProperty.JGroupInfinispanInitialHosts = formatJGroupsInitialHosts( + info.getNodeip(), info.getPeerip(), Integer.parseInt(SearchGlobalProperty.JGroupInfinispanPort)); + SearchGlobalProperty.JGroupBackendInitialHosts = formatJGroupsInitialHosts( + info.getNodeip(), info.getPeerip(), Integer.parseInt(SearchGlobalProperty.JGroupBackendPort)); if (getGlobalProperty("JGroup.TcppingInitialHosts") == null) { System.setProperty("JGroup.InfinispanInitialHosts", SearchGlobalProperty.JGroupInfinispanInitialHosts); logger.debug(String.format("default JGroup.InfinispanInitialHosts to JGroup.InfinispanInitialHosts [%s]", SearchGlobalProperty.JGroupInfinispanInitialHosts)); @@ -785,18 +793,14 @@ public static boolean isVIPNode() { return false; } - private static String getManagementServerCidrInternal() { - String mgtIp = getManagementServerIp(); - - /*# ip add | grep 10.86.4.132 - inet 10.86.4.132/23 brd 10.86.5.255 scope global br_eth0*/ - /* because Linux.shell can not run command with '|', pares the output of ip address in java */ - Linux.ShellResult ret = Linux.shell("ip -4 add"); + private static String getManagementServerCidrInternal(String mgtIp) { + String command = IPv6NetworkUtils.isIpv6Address(mgtIp) ? IPV6_ADDRESS_COMMAND : IPV4_ADDRESS_COMMAND; + Linux.ShellResult ret = Linux.shell(command); for (String line : ret.getStdout().split("\\n")) { if (line.contains(mgtIp)) { line = line.trim(); try { - return NetworkUtils.getNetworkAddressFromCidr(line.split(" ")[1]); + return NetworkUtils.getNetworkAddressFromCidr(line.split(" ")[IP_ADDRESS_COMMAND_CIDR_INDEX]); } catch (RuntimeException e) { return null; } @@ -808,29 +812,44 @@ private static String getManagementServerCidrInternal() { public static String getManagementServerCidr() { if (managementServerCidr == null) { - managementServerCidr = getManagementServerCidrInternal(); + managementServerCidr = getManagementServerCidrInternal(getManagementServerIp()); } return managementServerCidr; } + public static String getManagementServerCidr(String managementIp) { + return getManagementServerCidrInternal(normalizeManagementIp(managementIp)); + } + + public static String getManagementServerCidr(int ipVersion) { + String currentIp = getManagementServerIp(); + if ((ipVersion == IPv6Constants.IPv6 && IPv6NetworkUtils.isIpv6Address(currentIp)) || + (ipVersion == IPv6Constants.IPv4 && NetworkUtils.isIpv4Address(currentIp))) { + return getManagementServerCidr(currentIp); + } + + String ip = ipVersion == IPv6Constants.IPv6 ? getManagementServerIp6() : getManagementServerIp4(); + return ip == null ? null : getManagementServerCidr(ip); + } + private static String getManagementServerIpInternal() { - String ip = System.getProperty("management.server.ip"); + String ip = System.getProperty(MANAGEMENT_SERVER_IP_PROPERTY); if (ip != null) { - logger.info(String.format("get management IP[%s] from Java property[management.server.ip]", ip)); - return ip; + logger.info(String.format("get management IP[%s] from Java property[%s]", ip, MANAGEMENT_SERVER_IP_PROPERTY)); + return normalizeManagementIp(ip); } - ip = System.getenv("ZSTACK_MANAGEMENT_SERVER_IP"); + ip = System.getenv(ZSTACK_MANAGEMENT_SERVER_IP_ENV); if (ip != null) { - logger.info(String.format("get management IP[%s] from environment variable[ZSTACK_MANAGEMENT_SERVER_IP]", ip)); - return ip; + logger.info(String.format("get management IP[%s] from environment variable[%s]", ip, ZSTACK_MANAGEMENT_SERVER_IP_ENV)); + return normalizeManagementIp(ip); } - Linux.ShellResult ret = Linux.shell("/sbin/ip route"); + Linux.ShellResult ret = Linux.shell(DEFAULT_ROUTE_COMMAND); String defaultLine = null; for (String s : ret.getStdout().split("\n")) { - if (s.contains("default via")) { + if (s.contains(DEFAULT_ROUTE_MARK)) { defaultLine = s; break; } @@ -846,14 +865,7 @@ private static String getManagementServerIpInternal() { for (NetworkInterface iface : Collections.list(nets)) { String name = iface.getName(); if (defaultLine.contains(name)) { - for (InetAddress ia : Collections.list(iface.getInetAddresses())) { - ip = ia.getHostAddress(); - if (ia instanceof Inet4Address) { - // we prefer IPv4 address - ip = ia.getHostAddress(); - break; - } - } + ip = selectManagementServerIp(Collections.list(iface.getInetAddresses()), isManagementServerPreferIpv6()); } } } catch (SocketException e) { @@ -868,6 +880,101 @@ private static String getManagementServerIpInternal() { return ip; } + public static String getManagementServerIp6() { + try { + Enumeration nets = NetworkInterface.getNetworkInterfaces(); + for (NetworkInterface iface : Collections.list(nets)) { + if (!iface.isUp()) { + continue; + } + for (InetAddress ia : Collections.list(iface.getInetAddresses())) { + if (!(ia instanceof Inet4Address) && !ia.isLoopbackAddress() && !ia.isLinkLocalAddress()) { + return normalizeManagementIp(ia.getHostAddress()); + } + } + } + } catch (SocketException e) { + throw new CloudRuntimeException(e); + } + + return null; + } + + private static String getManagementServerIp4() { + try { + Enumeration nets = NetworkInterface.getNetworkInterfaces(); + for (NetworkInterface iface : Collections.list(nets)) { + if (!iface.isUp()) { + continue; + } + for (InetAddress ia : Collections.list(iface.getInetAddresses())) { + if (ia instanceof Inet4Address && !ia.isLoopbackAddress() && !ia.isLinkLocalAddress()) { + return normalizeManagementIp(ia.getHostAddress()); + } + } + } + } catch (SocketException e) { + throw new CloudRuntimeException(e); + } + + return null; + } + + public static String getManagementServerIp6Cidr() { + String ip6 = getManagementServerIp6(); + return ip6 == null ? null : getManagementServerCidr(ip6); + } + + public static String selectManagementServerIp(Collection addresses, boolean preferIpv6) { + String ipv4 = null; + String ipv6 = null; + + for (InetAddress address : addresses) { + String hostAddress = normalizeManagementIp(address.getHostAddress()); + if (address instanceof Inet4Address) { + ipv4 = hostAddress; + } else if (!IPv6NetworkUtils.isLinkLocalAddress(hostAddress)) { + ipv6 = hostAddress; + } + } + + if (preferIpv6 && ipv6 != null) { + return ipv6; + } + + return ipv4 != null ? ipv4 : ipv6; + } + + public static boolean isManagementServerPreferIpv6() { + String propertyValue = System.getProperty(MANAGEMENT_SERVER_PREFER_IPV6_PROPERTY); + if (propertyValue != null) { + return Boolean.parseBoolean(propertyValue); + } + + try { + return NetworkGlobalConfig.PREFER_IPV6.value(Boolean.class); + } catch (Throwable ignored) { + return false; + } + } + + public static String formatJGroupsInitialHosts(String nodeIp, String peerIp, int port) { + return String.format(JGROUPS_INITIAL_HOST_FORMAT, + IPv6NetworkUtils.formatHostForUrl(nodeIp), port, + IPv6NetworkUtils.formatHostForUrl(peerIp), port); + } + + private static String normalizeManagementIp(String ip) { + if (ip == null) { + return null; + } + int scopeIndex = ip.indexOf('%'); + if (scopeIndex >= 0) { + ip = ip.substring(0, scopeIndex); + } + return IPv6NetworkUtils.isIpv6Address(ip) ? IPv6NetworkUtils.normalizeIpv6(ip) : ip; + } + public static String toI18nString(String code, Object... args) { return toI18nString(code, null, args); } diff --git a/core/src/main/java/org/zstack/core/rest/RESTFacadeImpl.java b/core/src/main/java/org/zstack/core/rest/RESTFacadeImpl.java index 516fd500ef6..f66be622706 100755 --- a/core/src/main/java/org/zstack/core/rest/RESTFacadeImpl.java +++ b/core/src/main/java/org/zstack/core/rest/RESTFacadeImpl.java @@ -47,6 +47,7 @@ import org.zstack.utils.Utils; import org.zstack.utils.gson.JSONObjectUtil; import org.zstack.utils.logging.CLogger; +import org.zstack.utils.network.IPv6NetworkUtils; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; @@ -192,11 +193,9 @@ void init() { callbackHostName = hostname.trim(); } - String url; - if ("".equals(path) || path == null) { - url = String.format("http://%s:%s", callbackHostName, port); - } else { - url = String.format("http://%s:%s/%s", callbackHostName, port, path); + String url = IPv6NetworkUtils.buildHttpUrl(callbackHostName, port); + if (path != null && !path.isEmpty()) { + url = String.format("%s/%s", url, path); } UriComponentsBuilder ub = UriComponentsBuilder.fromHttpUrl(url); ub.path(RESTConstant.CALLBACK_PATH); diff --git a/plugin/applianceVm/src/main/java/org/zstack/appliancevm/ApplianceVmConstant.java b/plugin/applianceVm/src/main/java/org/zstack/appliancevm/ApplianceVmConstant.java index dcada8edeee..272d43264b2 100755 --- a/plugin/applianceVm/src/main/java/org/zstack/appliancevm/ApplianceVmConstant.java +++ b/plugin/applianceVm/src/main/java/org/zstack/appliancevm/ApplianceVmConstant.java @@ -33,6 +33,7 @@ public enum BootstrapParams { managementNodeIp, managementNodeVip, managementNodeCidr, + managementNodeIp6Cidr, additionalL3Uuids, } diff --git a/plugin/applianceVm/src/main/java/org/zstack/appliancevm/ApplianceVmFacadeImpl.java b/plugin/applianceVm/src/main/java/org/zstack/appliancevm/ApplianceVmFacadeImpl.java index 6d63ce3522f..7e46d12fd2a 100755 --- a/plugin/applianceVm/src/main/java/org/zstack/appliancevm/ApplianceVmFacadeImpl.java +++ b/plugin/applianceVm/src/main/java/org/zstack/appliancevm/ApplianceVmFacadeImpl.java @@ -464,6 +464,10 @@ public Map prepareBootstrapInformation(VmInstanceSpec spec) { ret.put(BootstrapParams.managementNodeIp.toString(), Platform.getManagementServerIp()); ret.put(BootstrapParams.managementNodeVip.toString(), Platform.getManagementServerVip()); ret.put(BootstrapParams.managementNodeCidr.toString(), Platform.getManagementServerCidr()); + String managementNodeIp6Cidr = Platform.getManagementServerIp6Cidr(); + if (managementNodeIp6Cidr != null) { + ret.put(BootstrapParams.managementNodeIp6Cidr.toString(), managementNodeIp6Cidr); + } /* this is only used by ApplianceVmPrepareBootstrapInfoExtensionPoint extension point, will be deleted after extension point */ ret.put(BootstrapParams.additionalL3Uuids.toString(), additionalNics.stream().map(VmNicInventory::getL3NetworkUuid).collect(Collectors.toList())); diff --git a/plugin/ceph/src/main/java/org/zstack/storage/ceph/backup/CephBackupStorageMonBase.java b/plugin/ceph/src/main/java/org/zstack/storage/ceph/backup/CephBackupStorageMonBase.java index 6bf2c810034..150c27ab41f 100755 --- a/plugin/ceph/src/main/java/org/zstack/storage/ceph/backup/CephBackupStorageMonBase.java +++ b/plugin/ceph/src/main/java/org/zstack/storage/ceph/backup/CephBackupStorageMonBase.java @@ -28,6 +28,7 @@ import org.zstack.storage.ceph.*; import org.zstack.utils.Utils; import org.zstack.utils.logging.CLogger; +import org.zstack.utils.network.IPv6NetworkUtils; import org.zstack.utils.path.PathUtil; import org.zstack.utils.ssh.Ssh; import org.zstack.utils.ssh.SshException; @@ -494,7 +495,7 @@ public void doPing(final ReturnValueCompletion completion) { } PingCmd cmd = new PingCmd(); - cmd.monAddr = String.format("%s:%s", getSelf().getMonAddr(), getSelf().getMonPort()); + cmd.monAddr = IPv6NetworkUtils.formatHostPort(getSelf().getMonAddr(), getSelf().getMonPort()); cmd.testImagePath = String.format("%s/zshb.bs.%s.%s", poolName, self.getUuid(), self.getMonAddr()); cmd.monUuid = getSelf().getUuid(); cmd.backupStorageUuid = getSelf().getBackupStorageUuid(); diff --git a/plugin/ceph/src/main/java/org/zstack/storage/ceph/primary/CephPrimaryStorageBase.java b/plugin/ceph/src/main/java/org/zstack/storage/ceph/primary/CephPrimaryStorageBase.java index c3b01dc3c8b..285e970de68 100755 --- a/plugin/ceph/src/main/java/org/zstack/storage/ceph/primary/CephPrimaryStorageBase.java +++ b/plugin/ceph/src/main/java/org/zstack/storage/ceph/primary/CephPrimaryStorageBase.java @@ -87,6 +87,7 @@ import org.zstack.utils.function.Function; import org.zstack.utils.gson.JSONObjectUtil; import org.zstack.utils.logging.CLogger; +import org.zstack.utils.network.IPv6NetworkUtils; import org.zstack.utils.network.NetworkUtils; import java.io.Serializable; @@ -4822,7 +4823,7 @@ private void setupSelfFencerOnKvmHost(final SetupSelfFencerOnKvmHostMsg msg, fin cmd.monUrls = CollectionUtils.transformToList(getSelf().getMons(), new Function() { @Override public String call(CephPrimaryStorageMonVO arg) { - return String.format("%s:%s", arg.getMonAddr(), arg.getMonPort()); + return IPv6NetworkUtils.formatHostPort(arg.getMonAddr(), arg.getMonPort()); } }); cmd.strategy = param.getStrategy(); @@ -4987,7 +4988,7 @@ private void checkHostStorageConnection(List hostUuids, final Completion .eq(CephPrimaryStoragePoolVO_.primaryStorageUuid, self.getUuid()) .listValues()) .monUrls(CollectionUtils.transformToList(getSelf().getMons(), (Function) arg - -> String.format("%s:%s", arg.getMonAddr(), arg.getMonPort()))); + -> IPv6NetworkUtils.formatHostPort(arg.getMonAddr(), arg.getMonPort()))); List msgs = CollectionUtils.transformToList(hostUuids, (Function) huuid -> { KVMHostAsyncHttpCallMsg msg = new KVMHostAsyncHttpCallMsg(); diff --git a/plugin/ceph/src/main/java/org/zstack/storage/ceph/primary/CephPrimaryStorageMonBase.java b/plugin/ceph/src/main/java/org/zstack/storage/ceph/primary/CephPrimaryStorageMonBase.java index 7ca3e710291..e3ba7578f12 100755 --- a/plugin/ceph/src/main/java/org/zstack/storage/ceph/primary/CephPrimaryStorageMonBase.java +++ b/plugin/ceph/src/main/java/org/zstack/storage/ceph/primary/CephPrimaryStorageMonBase.java @@ -26,6 +26,7 @@ import org.zstack.storage.ceph.*; import org.zstack.utils.Utils; import org.zstack.utils.logging.CLogger; +import org.zstack.utils.network.IPv6NetworkUtils; import org.zstack.utils.path.PathUtil; import org.zstack.utils.ssh.Ssh; import org.zstack.utils.ssh.SshException; @@ -506,7 +507,7 @@ private void doPing(final ReturnValueCompletion completion) { cmd.testImagePath = String.format("%s/zshb.ps.%s.%s", poolName, self.getUuid(), self.getMonAddr()); cmd.monUuid = getSelf().getUuid(); cmd.primaryStorageUuid = getSelf().getPrimaryStorageUuid(); - cmd.monAddr = String.format("%s:%s", getSelf().getMonAddr(), getSelf().getMonPort()); + cmd.monAddr = IPv6NetworkUtils.formatHostPort(getSelf().getMonAddr(), getSelf().getMonPort()); restf.asyncJsonPost(CephAgentUrl.primaryStorageUrl(self.getHostname(), PING_PATH), cmd, new JsonAsyncRESTCallback(completion) { diff --git a/plugin/kvm/src/main/java/org/zstack/kvm/KVMHost.java b/plugin/kvm/src/main/java/org/zstack/kvm/KVMHost.java index fb9c12d1c73..59ce9aad88b 100755 --- a/plugin/kvm/src/main/java/org/zstack/kvm/KVMHost.java +++ b/plugin/kvm/src/main/java/org/zstack/kvm/KVMHost.java @@ -2207,7 +2207,7 @@ protected static String getDataNetworkAddress(String hostUuid, String cidr) { final String[] ips = extraIps.split(","); for (String ip: ips) { - if (NetworkUtils.isIpv4InCidr(ip, cidr)) { + if (NetworkUtils.isIpInCidr(ip, cidr)) { return ip; } } @@ -6779,7 +6779,7 @@ public void handle(ErrorCode errCode, Map data) { } private boolean checkMigrateNetworkCidrOfHost(String cidr) { - if (NetworkUtils.isIpv4InCidr(self.getManagementIp(), cidr)) { + if (NetworkUtils.isIpInCidr(self.getManagementIp(), cidr)) { return true; } @@ -6792,7 +6792,7 @@ private boolean checkMigrateNetworkCidrOfHost(String cidr) { final String[] ips = extraIps.split(","); for (String ip: ips) { - if (NetworkUtils.isIpv4InCidr(ip, cidr)) { + if (NetworkUtils.isIpInCidr(ip, cidr)) { return true; } } diff --git a/plugin/kvm/src/main/java/org/zstack/kvm/KVMHostUtils.java b/plugin/kvm/src/main/java/org/zstack/kvm/KVMHostUtils.java index 54a3ed0bcce..0ed2594122f 100644 --- a/plugin/kvm/src/main/java/org/zstack/kvm/KVMHostUtils.java +++ b/plugin/kvm/src/main/java/org/zstack/kvm/KVMHostUtils.java @@ -27,7 +27,6 @@ */ public class KVMHostUtils { private static final CLogger logger = CLoggerImpl.getLogger(KVMHostUtils.class); - private static final String URL_IPV6_HOST_FORMAT = "[%s]"; // ZSTAC-84446: br_conn_all_ns is host-internal; exclude from TLS cert SAN // to keep check-flow and deploy-flow IP lists identical. @@ -137,7 +136,7 @@ public static String collectHostIps(String hostUuid, String managementIp, } public static String formatHostForUrl(String host) { - return IPv6NetworkUtils.isIpv6Address(host) ? String.format(URL_IPV6_HOST_FORMAT, host) : host; + return IPv6NetworkUtils.formatHostForUrl(host); } // ZSTAC-84446: force ansible re-run + libvirtd restart only when operator opted in diff --git a/plugin/nfsPrimaryStorage/src/main/java/org/zstack/storage/primary/nfs/NfsApiParamChecker.java b/plugin/nfsPrimaryStorage/src/main/java/org/zstack/storage/primary/nfs/NfsApiParamChecker.java index 00f43fcf595..dca56b1bb91 100755 --- a/plugin/nfsPrimaryStorage/src/main/java/org/zstack/storage/primary/nfs/NfsApiParamChecker.java +++ b/plugin/nfsPrimaryStorage/src/main/java/org/zstack/storage/primary/nfs/NfsApiParamChecker.java @@ -32,6 +32,10 @@ @Configurable(preConstruction = true, autowire = Autowire.BY_TYPE) public class NfsApiParamChecker { + private static final String IPV6_URL_HOST_PREFIX = "["; + private static final String IPV6_URL_HOST_SUFFIX = "]"; + private static final String NFS_URL_SEPARATOR = ":"; + @Autowired private DatabaseFacade dbf; @Autowired @@ -47,13 +51,13 @@ public void checkUrl(String zoneUuid, List systemTags, String url) { throw new ApiMessageInterceptionException(argerr(ORG_ZSTACK_STORAGE_PRIMARY_NFS_10006, "there has been a nfs primary storage having url as %s in zone[uuid:%s]", url, zoneUuid)); } - String[] results = url.split(":"); - if (results.length == 2 && ( - results[1].startsWith("/dev") || results[1].startsWith("/proc") || results[1].startsWith("/sys"))) { + String path = getNfsPath(url); + if (path != null && ( + path.startsWith("/dev") || path.startsWith("/proc") || path.startsWith("/sys"))) { throw new ApiMessageInterceptionException(argerr(ORG_ZSTACK_STORAGE_PRIMARY_NFS_10007, " the url contains an invalid folder[/dev or /proc or /sys]")); } - validateUrl(systemTags, results[0]); + validateUrl(systemTags, getNfsHost(url)); } @@ -79,11 +83,38 @@ private void validateCidrTag(String sysTag, String ipAddr) { throw new ApiMessageInterceptionException(argerr(ORG_ZSTACK_STORAGE_PRIMARY_NFS_10009, "invalid CIDR: %s", cidr)); } - if (!NetworkUtils.isIpv4InCidr(ipAddr, cidr)) { + if (!NetworkUtils.isIpInCidr(ipAddr, cidr)) { throw new ApiMessageInterceptionException(argerr(ORG_ZSTACK_STORAGE_PRIMARY_NFS_10010, "IP address[%s] is not in CIDR[%s]", ipAddr, cidr)); } } + private String getNfsHost(String url) { + if (url.startsWith(IPV6_URL_HOST_PREFIX)) { + int end = url.indexOf(IPV6_URL_HOST_SUFFIX); + if (end > 0) { + return url.substring(IPV6_URL_HOST_PREFIX.length(), end); + } + } + + return url.split(NFS_URL_SEPARATOR)[0]; + } + + private String getNfsPath(String url) { + if (url.startsWith(IPV6_URL_HOST_PREFIX)) { + int end = url.indexOf(IPV6_URL_HOST_SUFFIX); + if (end > 0 && url.length() > end + IPV6_URL_HOST_SUFFIX.length()) { + String suffix = url.substring(end + IPV6_URL_HOST_SUFFIX.length()); + if (suffix.startsWith(NFS_URL_SEPARATOR)) { + return suffix.substring(NFS_URL_SEPARATOR.length()); + } + } + return null; + } + + String[] results = url.split(NFS_URL_SEPARATOR); + return results.length == 2 ? results[1] : null; + } + public void checkRunningVmForUpdateUrl(String psuuid) { String sql = "select vm.name, vm.uuid from VmInstanceVO vm, VolumeVO vol where vm.uuid = vol.vmInstanceUuid and" + " vol.primaryStorageUuid = :psUuid and vm.state = :vmState"; diff --git a/plugin/vxlan/src/main/java/org/zstack/network/l2/vxlan/vxlanNetworkPool/VxlanPoolApiInterceptor.java b/plugin/vxlan/src/main/java/org/zstack/network/l2/vxlan/vxlanNetworkPool/VxlanPoolApiInterceptor.java index 6629d0f7def..5f9a920b81b 100644 --- a/plugin/vxlan/src/main/java/org/zstack/network/l2/vxlan/vxlanNetworkPool/VxlanPoolApiInterceptor.java +++ b/plugin/vxlan/src/main/java/org/zstack/network/l2/vxlan/vxlanNetworkPool/VxlanPoolApiInterceptor.java @@ -56,9 +56,8 @@ public APIMessage intercept(APIMessage msg) throws ApiMessageInterceptionExcepti } private void validate(APICreateVxlanPoolRemoteVtepMsg msg) { - boolean isIpv4 = NetworkUtils.isIpv4Address(msg.getRemoteVtepIp()); - if (!isIpv4) { - throw new ApiMessageInterceptionException(argerr(ORG_ZSTACK_NETWORK_L2_VXLAN_VXLANNETWORKPOOL_10015, "%s:is not ipv4", msg.getRemoteVtepIp())); + if (!NetworkUtils.isIpAddress(msg.getRemoteVtepIp())) { + throw new ApiMessageInterceptionException(argerr(ORG_ZSTACK_NETWORK_L2_VXLAN_VXLANNETWORKPOOL_10015, "%s is not a valid IP address", msg.getRemoteVtepIp())); } SimpleQuery rqv = dbf.createQuery(VtepVO.class); @@ -73,9 +72,8 @@ private void validate(APICreateVxlanPoolRemoteVtepMsg msg) { } private void validate(APIDeleteVxlanPoolRemoteVtepMsg msg) { - boolean isIpv4 = NetworkUtils.isIpv4Address(msg.getRemoteVtepIp()); - if (!isIpv4) { - throw new ApiMessageInterceptionException(argerr(ORG_ZSTACK_NETWORK_L2_VXLAN_VXLANNETWORKPOOL_10017, "%s:is not ipv4", msg.getRemoteVtepIp())); + if (!NetworkUtils.isIpAddress(msg.getRemoteVtepIp())) { + throw new ApiMessageInterceptionException(argerr(ORG_ZSTACK_NETWORK_L2_VXLAN_VXLANNETWORKPOOL_10017, "%s is not a valid IP address", msg.getRemoteVtepIp())); } } diff --git a/test/src/test/groovy/org/zstack/test/integration/core/ManagementNetworkIpv6Case.groovy b/test/src/test/groovy/org/zstack/test/integration/core/ManagementNetworkIpv6Case.groovy new file mode 100644 index 00000000000..8aae66074df --- /dev/null +++ b/test/src/test/groovy/org/zstack/test/integration/core/ManagementNetworkIpv6Case.groovy @@ -0,0 +1,98 @@ +package org.zstack.test.integration.core + +import org.zstack.appliancevm.ApplianceVmConstant +import org.zstack.core.NetworkGlobalConfig +import org.zstack.core.Platform +import org.zstack.utils.network.IPv6Constants +import org.zstack.utils.network.IPv6NetworkUtils +import org.zstack.utils.network.NetworkUtils +import org.junit.Test + +class ManagementNetworkIpv6Case { + private static final String IPV4 = "192.168.1.10" + private static final String IPV6 = "2001:db8::1" + private static final String IPV6_FULL = "2001:0db8:0000:0000:0000:0000:0000:0001" + private static final String LINK_LOCAL_IPV6 = "fe80::1" + private static final String INVALID_IP = "not-an-ip!!" + private static final int REST_PORT = 8080 + private static final int JGROUP_PORT = 7805 + + @Test + void test() { + testPreferIpv6DefaultFalse() + testBuildUrlIpv4() + testBuildUrlIpv6() + testBuildHostPortIpv6() + testBracketIpv6Idempotent() + testNormalizeIpv6() + testManagementEndpointValidation() + testJGroupsInitialHostsIpv6Format() + testJGroupsInitialHostsIpv4Regression() + testIpv6NetworkCidr() + testIpInCidrDualStack() + testManagementCidrIpVersionOverload() + testApplianceVmBootstrapParam() + } + + void testPreferIpv6DefaultFalse() { + assert NetworkGlobalConfig.PREFER_IPV6.getIdentity() == "managementServer.prefer.ipv6" + } + + void testBuildUrlIpv4() { + assert IPv6NetworkUtils.buildHttpUrl(IPV4, REST_PORT) == "http://192.168.1.10:8080" + } + + void testBuildUrlIpv6() { + assert IPv6NetworkUtils.buildHttpUrl(IPV6, REST_PORT) == "http://[2001:db8::1]:8080" + } + + void testBuildHostPortIpv6() { + assert IPv6NetworkUtils.formatHostPort(IPV6, REST_PORT) == "[2001:db8::1]:8080" + } + + void testBracketIpv6Idempotent() { + assert IPv6NetworkUtils.formatHostForUrl(IPV6) == "[2001:db8::1]" + assert IPv6NetworkUtils.formatHostForUrl("[2001:db8::1]") == "[2001:db8::1]" + } + + void testNormalizeIpv6() { + assert IPv6NetworkUtils.normalizeIpv6(IPV6_FULL) == IPV6 + } + + void testManagementEndpointValidation() { + assert IPv6NetworkUtils.isValidManagementEndpoint(IPV4) + assert IPv6NetworkUtils.isValidManagementEndpoint(IPV6) + assert IPv6NetworkUtils.isValidManagementEndpoint("host-01.example.com") + assert !IPv6NetworkUtils.isValidManagementEndpoint(LINK_LOCAL_IPV6) + assert !IPv6NetworkUtils.isValidManagementEndpoint(INVALID_IP) + } + + void testJGroupsInitialHostsIpv6Format() { + assert Platform.formatJGroupsInitialHosts(IPV6, "2001:db8::2", JGROUP_PORT) == + "[2001:db8::1][7805],[2001:db8::2][7805]" + } + + void testJGroupsInitialHostsIpv4Regression() { + assert Platform.formatJGroupsInitialHosts(IPV4, "192.168.1.11", JGROUP_PORT) == + "192.168.1.10[7805],192.168.1.11[7805]" + } + + void testIpv6NetworkCidr() { + assert NetworkUtils.getNetworkAddressFromCidr("2001:db8::1/64") == "2001:db8::/64" + } + + void testIpInCidrDualStack() { + assert NetworkUtils.isIpInCidr(IPV4, "192.168.1.0/24") + assert NetworkUtils.isIpInCidr(IPV6, "2001:db8::/64") + assert !NetworkUtils.isIpInCidr(IPV4, "2001:db8::/64") + assert !NetworkUtils.isIpInCidr(IPV6, "192.168.1.0/24") + } + + void testManagementCidrIpVersionOverload() { + assert Platform.getManagementServerCidr(IPv6Constants.IPv4) == Platform.getManagementServerCidr(Platform.getManagementServerIp()) + } + + void testApplianceVmBootstrapParam() { + assert ApplianceVmConstant.BootstrapParams.managementNodeIp6Cidr.toString() == "managementNodeIp6Cidr" + } +} diff --git a/utils/src/main/java/org/zstack/utils/network/IPv6NetworkUtils.java b/utils/src/main/java/org/zstack/utils/network/IPv6NetworkUtils.java index 50bbe6f195b..beea7545e59 100644 --- a/utils/src/main/java/org/zstack/utils/network/IPv6NetworkUtils.java +++ b/utils/src/main/java/org/zstack/utils/network/IPv6NetworkUtils.java @@ -13,6 +13,11 @@ public class IPv6NetworkUtils { private final static CLogger logger = Utils.getLogger(IPv6NetworkUtils.class); + private static final String URL_IPV6_HOST_FORMAT = "[%s]"; + private static final String HTTP_URL_FORMAT = "http://%s:%s"; + private static final String HOST_PORT_FORMAT = "%s:%s"; + private static final String IPV6_BRACKET_PREFIX = "["; + private static final String IPV6_BRACKET_SUFFIX = "]"; // IPv4 地址正则表达式 private static String ipv4Regex = "^(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$"; @@ -134,6 +139,9 @@ public static String getIPv6AddresFromMac(String networkCidr, String mac) { } public static boolean isIpv6Address(String ip) { + if (ip == null) { + return false; + } try { IPv6Address.fromString(ip); return true; @@ -143,6 +151,9 @@ public static boolean isIpv6Address(String ip) { } public static boolean isIpv6UnicastAddress(String ip) { + if (ip == null) { + return false; + } try { IPv6Address address = IPv6Address.fromString(ip); if (address.isMulticast() || address.isLinkLocal() || address.isSiteLocal()) { @@ -500,4 +511,36 @@ public static String getIpByIpVersion(String ipVersion, List ipList) { public static boolean isFullCidr(String cidr) { return cidr.equals("::/0"); } + + public static String normalizeIpv6(String ip) { + return getIpv6AddressCanonicalString(ip); + } + + public static String formatHostForUrl(String host) { + if (host == null) { + return null; + } + + if (host.startsWith(IPV6_BRACKET_PREFIX) && host.endsWith(IPV6_BRACKET_SUFFIX)) { + return host; + } + + return isIpv6Address(host) ? String.format(URL_IPV6_HOST_FORMAT, host) : host; + } + + public static String buildHttpUrl(String host, int port) { + return String.format(HTTP_URL_FORMAT, formatHostForUrl(host), port); + } + + public static String formatHostPort(String host, int port) { + return String.format(HOST_PORT_FORMAT, formatHostForUrl(host), port); + } + + public static boolean isValidManagementEndpoint(String endpoint) { + if (NetworkUtils.isIpv4Address(endpoint) || NetworkUtils.isHostname(endpoint)) { + return true; + } + + return isIpv6Address(endpoint) && !isLinkLocalAddress(endpoint); + } } diff --git a/utils/src/main/java/org/zstack/utils/network/NetworkUtils.java b/utils/src/main/java/org/zstack/utils/network/NetworkUtils.java index 3a7859fec8d..0bc3daef0c2 100755 --- a/utils/src/main/java/org/zstack/utils/network/NetworkUtils.java +++ b/utils/src/main/java/org/zstack/utils/network/NetworkUtils.java @@ -558,6 +558,23 @@ public static boolean isIpv4InCidr(String ipv4, String cidr) { return isIpv4InRange(ipv4, info.getLowAddress(), info.getHighAddress()); } + public static boolean isIpInCidr(String ip, String cidr) { + DebugUtils.Assert(isCidr(cidr), String.format("%s is not a cidr", cidr)); + validateIp(ip); + + if (isIpv4Address(ip)) { + if (!isCidr(cidr, IPv6Constants.IPv4)) { + return false; + } + return isIpv4InCidr(ip, cidr); + } + + if (!isCidr(cidr, IPv6Constants.IPv6)) { + return false; + } + return IPv6NetworkUtils.isIpv6InCidrRange(ip, cidr); + } + public static List filterIpv4sInCidr(List ipv4s, String cidr){ DebugUtils.Assert(isCidr(cidr), String.format("%s is not a cidr", cidr)); SubnetUtils.SubnetInfo info = getSubnetInfo(new SubnetUtils(cidr)); @@ -588,6 +605,10 @@ public static boolean isSubCidr(String cidr, String subCidr) { public static String getNetworkAddressFromCidr(String cidr) { DebugUtils.Assert(isCidr(cidr), String.format("%s is not a cidr", cidr)); + if (isIpv6Address(cidr.split("\\/")[0])) { + return IPv6NetworkUtils.getFormalCidrOfNetworkCidr(cidr); + } + SubnetUtils n = new SubnetUtils(cidr); return String.format("%s/%s", n.getInfo().getNetworkAddress(), cidr.split("\\/")[1]); } @@ -972,4 +993,3 @@ public static int compareIpv4Address(String ip1, String ip2) { return diff > 0 ? 1 : diff == 0 ? 0 : -1; } } - diff --git a/utils/src/main/java/org/zstack/utils/zsha2/ZSha2Helper.java b/utils/src/main/java/org/zstack/utils/zsha2/ZSha2Helper.java index 5e9d4b9a4dc..c14290d71bc 100644 --- a/utils/src/main/java/org/zstack/utils/zsha2/ZSha2Helper.java +++ b/utils/src/main/java/org/zstack/utils/zsha2/ZSha2Helper.java @@ -39,7 +39,7 @@ public static ZSha2Info getInfo(boolean checkZSha2Status) { ZSha2Info info = JSONObjectUtil.toObject(result.getStdout(), ZSha2Info.class); info.setMaster(ShellUtils.runAndReturn(String.format( - "ip addr show %s | grep -q '[^0-9]%s[^0-9]'", info.getNic(), info.getDbvip())).isReturnCode(0)); + "ip addr show %s | grep -q ' %s/'", info.getNic(), info.getDbvip())).isReturnCode(0)); return info; } From 912b19f92c6cdb189aaf97c5ab1062bde3fbb8f5 Mon Sep 17 00:00:00 2001 From: "shixin.ruan" Date: Thu, 21 May 2026 17:35:48 +0900 Subject: [PATCH 03/53] [mgt-ipv6]: dedicate vxlan error codes Use new VXLAN error code constants for remote VTEP invalid IP validation so the new messages do not reuse existing ErrorCodes. Resolves: ZSTAC-79206 Change-Id: Ibd6ea314eda79a25e56f3e75d13e0101ca0c4b44 --- .../l2/vxlan/vxlanNetworkPool/VxlanPoolApiInterceptor.java | 4 ++-- .../zstack/utils/clouderrorcode/CloudOperationsErrorCode.java | 4 ++++ 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/plugin/vxlan/src/main/java/org/zstack/network/l2/vxlan/vxlanNetworkPool/VxlanPoolApiInterceptor.java b/plugin/vxlan/src/main/java/org/zstack/network/l2/vxlan/vxlanNetworkPool/VxlanPoolApiInterceptor.java index 5f9a920b81b..8310624e1a4 100644 --- a/plugin/vxlan/src/main/java/org/zstack/network/l2/vxlan/vxlanNetworkPool/VxlanPoolApiInterceptor.java +++ b/plugin/vxlan/src/main/java/org/zstack/network/l2/vxlan/vxlanNetworkPool/VxlanPoolApiInterceptor.java @@ -57,7 +57,7 @@ public APIMessage intercept(APIMessage msg) throws ApiMessageInterceptionExcepti private void validate(APICreateVxlanPoolRemoteVtepMsg msg) { if (!NetworkUtils.isIpAddress(msg.getRemoteVtepIp())) { - throw new ApiMessageInterceptionException(argerr(ORG_ZSTACK_NETWORK_L2_VXLAN_VXLANNETWORKPOOL_10015, "%s is not a valid IP address", msg.getRemoteVtepIp())); + throw new ApiMessageInterceptionException(argerr(ORG_ZSTACK_NETWORK_L2_VXLAN_VXLANNETWORKPOOL_10032, "%s is not a valid IP address", msg.getRemoteVtepIp())); } SimpleQuery rqv = dbf.createQuery(VtepVO.class); @@ -73,7 +73,7 @@ private void validate(APICreateVxlanPoolRemoteVtepMsg msg) { private void validate(APIDeleteVxlanPoolRemoteVtepMsg msg) { if (!NetworkUtils.isIpAddress(msg.getRemoteVtepIp())) { - throw new ApiMessageInterceptionException(argerr(ORG_ZSTACK_NETWORK_L2_VXLAN_VXLANNETWORKPOOL_10017, "%s is not a valid IP address", msg.getRemoteVtepIp())); + throw new ApiMessageInterceptionException(argerr(ORG_ZSTACK_NETWORK_L2_VXLAN_VXLANNETWORKPOOL_10033, "%s is not a valid IP address", msg.getRemoteVtepIp())); } } diff --git a/utils/src/main/java/org/zstack/utils/clouderrorcode/CloudOperationsErrorCode.java b/utils/src/main/java/org/zstack/utils/clouderrorcode/CloudOperationsErrorCode.java index 0956a78d712..dccd67723ac 100644 --- a/utils/src/main/java/org/zstack/utils/clouderrorcode/CloudOperationsErrorCode.java +++ b/utils/src/main/java/org/zstack/utils/clouderrorcode/CloudOperationsErrorCode.java @@ -7824,6 +7824,10 @@ public class CloudOperationsErrorCode { public static final String ORG_ZSTACK_NETWORK_L2_VXLAN_VXLANNETWORKPOOL_10031 = "ORG_ZSTACK_NETWORK_L2_VXLAN_VXLANNETWORKPOOL_10031"; + public static final String ORG_ZSTACK_NETWORK_L2_VXLAN_VXLANNETWORKPOOL_10032 = "ORG_ZSTACK_NETWORK_L2_VXLAN_VXLANNETWORKPOOL_10032"; + + public static final String ORG_ZSTACK_NETWORK_L2_VXLAN_VXLANNETWORKPOOL_10033 = "ORG_ZSTACK_NETWORK_L2_VXLAN_VXLANNETWORKPOOL_10033"; + public static final String ORG_ZSTACK_IAM2_CONTAINER_ZAKU_10000 = "ORG_ZSTACK_IAM2_CONTAINER_ZAKU_10000"; public static final String ORG_ZSTACK_IAM2_CONTAINER_ZAKU_10001 = "ORG_ZSTACK_IAM2_CONTAINER_ZAKU_10001"; From 54d67cc38f77b656a45bd61aa7b512c0dc155781 Mon Sep 17 00:00:00 2001 From: "shixin.ruan" Date: Thu, 21 May 2026 19:27:20 +0900 Subject: [PATCH 04/53] [mgt-ipv6]: close vr bootstrap address flow Select appliance VM bootstrap managementNodeIp by VR management CIDR and reject non-routable IPv6 management endpoints. Resolves: ZSTAC-79206 Change-Id: If4f857d07b043519fc2f6a7b752246f35d0ced8e --- .../org/zstack/core/NetworkGlobalConfig.java | 2 +- .../main/java/org/zstack/core/Platform.java | 9 ++++ .../appliancevm/ApplianceVmFacadeImpl.java | 48 ++++++++++++++++- .../core/ManagementNetworkIpv6Case.groovy | 52 ++++++++++++++++++- .../kvm/host/KvmHostIpv6Case.groovy | 25 +++++++-- .../utils/network/IPv6NetworkUtils.java | 21 +++++++- 6 files changed, 147 insertions(+), 10 deletions(-) diff --git a/core/src/main/java/org/zstack/core/NetworkGlobalConfig.java b/core/src/main/java/org/zstack/core/NetworkGlobalConfig.java index 2195788fc47..1b5c78abce9 100644 --- a/core/src/main/java/org/zstack/core/NetworkGlobalConfig.java +++ b/core/src/main/java/org/zstack/core/NetworkGlobalConfig.java @@ -7,7 +7,7 @@ @GlobalConfigDefinition public class NetworkGlobalConfig { - public static final String CATEGORY = "managementServer"; + public static final String CATEGORY = "management.server"; @GlobalConfigValidation @GlobalConfigDef(defaultValue = "false", type = Boolean.class, description = "Prefer IPv6 for management server address selection") diff --git a/core/src/main/java/org/zstack/core/Platform.java b/core/src/main/java/org/zstack/core/Platform.java index 1f27ea79d14..82bd1047042 100755 --- a/core/src/main/java/org/zstack/core/Platform.java +++ b/core/src/main/java/org/zstack/core/Platform.java @@ -925,6 +925,15 @@ public static String getManagementServerIp6Cidr() { return ip6 == null ? null : getManagementServerCidr(ip6); } + public static List getManagementServerIps() { + LinkedHashSet ips = new LinkedHashSet<>(); + ips.add(getManagementServerIp()); + ips.add(getManagementServerIp4()); + ips.add(getManagementServerIp6()); + ips.remove(null); + return new ArrayList<>(ips); + } + public static String selectManagementServerIp(Collection addresses, boolean preferIpv6) { String ipv4 = null; String ipv6 = null; diff --git a/plugin/applianceVm/src/main/java/org/zstack/appliancevm/ApplianceVmFacadeImpl.java b/plugin/applianceVm/src/main/java/org/zstack/appliancevm/ApplianceVmFacadeImpl.java index 7e46d12fd2a..2c8b3e3c1d9 100755 --- a/plugin/applianceVm/src/main/java/org/zstack/appliancevm/ApplianceVmFacadeImpl.java +++ b/plugin/applianceVm/src/main/java/org/zstack/appliancevm/ApplianceVmFacadeImpl.java @@ -1,6 +1,7 @@ package org.zstack.appliancevm; import com.google.gson.Gson; +import org.apache.commons.lang.StringUtils; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.transaction.annotation.Transactional; import org.zstack.appliancevm.ApplianceVmConstant.BootstrapParams; @@ -31,6 +32,7 @@ import org.zstack.header.message.Message; import org.zstack.header.message.MessageReply; import org.zstack.header.network.l2.*; +import org.zstack.header.network.l3.UsedIpInventory; import org.zstack.header.network.l3.L3NetworkVO; import org.zstack.header.network.l3.L3NetworkVO_; import org.zstack.header.rest.RESTFacade; @@ -46,6 +48,8 @@ import org.zstack.utils.Utils; import org.zstack.utils.function.Function; import org.zstack.utils.logging.CLogger; +import org.zstack.utils.network.IPv6Constants; +import org.zstack.utils.network.NetworkUtils; import javax.persistence.Query; import java.util.*; @@ -59,6 +63,7 @@ */ public class ApplianceVmFacadeImpl extends AbstractService implements ApplianceVmFacade, Component { private static final CLogger logger = Utils.getLogger(ApplianceVmFacadeImpl.class); + private static final String NO_MATCHED_MN_IP_WARN = "no MN IP matched VR management CIDR %s, fallback to %s"; @Autowired private CloudBus bus; @@ -461,7 +466,10 @@ public Map prepareBootstrapInformation(VmInstanceSpec spec) { String publicKey = asf.getPublicKey(); ret.put(ApplianceVmConstant.BootstrapParams.publicKey.toString(), publicKey); ret.put(BootstrapParams.uuid.toString(), spec.getVmInventory().getUuid()); - ret.put(BootstrapParams.managementNodeIp.toString(), Platform.getManagementServerIp()); + ret.put(BootstrapParams.managementNodeIp.toString(), selectManagementNodeIpForBootstrap( + Platform.getManagementServerIps(), + getVrManagementCidrs(mgmtNic), + Platform.getManagementServerIp())); ret.put(BootstrapParams.managementNodeVip.toString(), Platform.getManagementServerVip()); ret.put(BootstrapParams.managementNodeCidr.toString(), Platform.getManagementServerCidr()); String managementNodeIp6Cidr = Platform.getManagementServerIp6Cidr(); @@ -479,6 +487,44 @@ public Map prepareBootstrapInformation(VmInstanceSpec spec) { return ret; } + public static String selectManagementNodeIpForBootstrap(Collection mnIps, Collection vrManagementCidrs, String fallbackIp) { + for (String cidr : vrManagementCidrs) { + if (!NetworkUtils.isCidr(cidr)) { + continue; + } + for (String ip : mnIps) { + if (StringUtils.isBlank(ip)) { + continue; + } + if (NetworkUtils.isIpInCidr(ip, cidr)) { + return ip; + } + } + } + + if (!vrManagementCidrs.isEmpty()) { + logger.warn(String.format(NO_MATCHED_MN_IP_WARN, String.join(",", vrManagementCidrs), fallbackIp)); + } + return fallbackIp; + } + + private Collection getVrManagementCidrs(VmNicInventory managementNic) { + List cidrs = new ArrayList<>(); + if (managementNic == null || managementNic.getUsedIps() == null) { + return cidrs; + } + + for (UsedIpInventory ip : managementNic.getUsedIps()) { + if (Objects.equals(ip.getIpVersion(), IPv6Constants.IPv4) && StringUtils.isNotBlank(ip.getNetmask())) { + cidrs.add(NetworkUtils.getCidrFromIpMask(ip.getIp(), ip.getNetmask())); + } else if (Objects.equals(ip.getIpVersion(), IPv6Constants.IPv6) && ip.getPrefixLen() != null) { + cidrs.add(NetworkUtils.getNetworkAddressFromCidr(String.format("%s/%s", ip.getIp(), ip.getPrefixLen()))); + } + } + + return cidrs; + } + @Override public Flow createBootstrapFlow(HypervisorType hvType) { diff --git a/test/src/test/groovy/org/zstack/test/integration/core/ManagementNetworkIpv6Case.groovy b/test/src/test/groovy/org/zstack/test/integration/core/ManagementNetworkIpv6Case.groovy index 8aae66074df..b02e3df7847 100644 --- a/test/src/test/groovy/org/zstack/test/integration/core/ManagementNetworkIpv6Case.groovy +++ b/test/src/test/groovy/org/zstack/test/integration/core/ManagementNetworkIpv6Case.groovy @@ -1,6 +1,7 @@ package org.zstack.test.integration.core import org.zstack.appliancevm.ApplianceVmConstant +import org.zstack.appliancevm.ApplianceVmFacadeImpl import org.zstack.core.NetworkGlobalConfig import org.zstack.core.Platform import org.zstack.utils.network.IPv6Constants @@ -11,8 +12,10 @@ import org.junit.Test class ManagementNetworkIpv6Case { private static final String IPV4 = "192.168.1.10" private static final String IPV6 = "2001:db8::1" + private static final String IPV6_2 = "2001:db8::2" private static final String IPV6_FULL = "2001:0db8:0000:0000:0000:0000:0000:0001" private static final String LINK_LOCAL_IPV6 = "fe80::1" + private static final String LOOPBACK_IPV6 = "::1" private static final String INVALID_IP = "not-an-ip!!" private static final int REST_PORT = 8080 private static final int JGROUP_PORT = 7805 @@ -20,6 +23,9 @@ class ManagementNetworkIpv6Case { @Test void test() { testPreferIpv6DefaultFalse() + testPreferIpv6SystemProperty() + testSelectManagementServerIpDualStackPolicy() + testSelectApplianceVmManagementNodeIpByCidr() testBuildUrlIpv4() testBuildUrlIpv6() testBuildHostPortIpv6() @@ -35,7 +41,48 @@ class ManagementNetworkIpv6Case { } void testPreferIpv6DefaultFalse() { - assert NetworkGlobalConfig.PREFER_IPV6.getIdentity() == "managementServer.prefer.ipv6" + assert NetworkGlobalConfig.PREFER_IPV6.getIdentity() == "management.server.prefer.ipv6" + } + + void testPreferIpv6SystemProperty() { + String oldValue = System.getProperty("management.server.prefer.ipv6") + try { + System.setProperty("management.server.prefer.ipv6", "true") + assert Platform.isManagementServerPreferIpv6() + System.setProperty("management.server.prefer.ipv6", "false") + assert !Platform.isManagementServerPreferIpv6() + } finally { + if (oldValue == null) { + System.clearProperty("management.server.prefer.ipv6") + } else { + System.setProperty("management.server.prefer.ipv6", oldValue) + } + } + } + + void testSelectManagementServerIpDualStackPolicy() { + def ipv4 = InetAddress.getByName(IPV4) + def ipv6 = InetAddress.getByName(IPV6) + + assert Platform.selectManagementServerIp([ipv6, ipv4], false) == IPV4 + assert Platform.selectManagementServerIp([ipv4, ipv6], true) == IPV6 + assert Platform.selectManagementServerIp([ipv6], false) == IPV6 + assert Platform.selectManagementServerIp([ipv4], true) == IPV4 + } + + void testSelectApplianceVmManagementNodeIpByCidr() { + assert ApplianceVmFacadeImpl.selectManagementNodeIpForBootstrap( + [IPV4, IPV6], + ["2001:db8::/64"], + IPV4) == IPV6 + assert ApplianceVmFacadeImpl.selectManagementNodeIpForBootstrap( + [IPV4, IPV6], + ["192.168.1.0/24"], + IPV6) == IPV4 + assert ApplianceVmFacadeImpl.selectManagementNodeIpForBootstrap( + [IPV4, IPV6], + ["10.0.0.0/24"], + IPV6) == IPV6 } void testBuildUrlIpv4() { @@ -64,11 +111,12 @@ class ManagementNetworkIpv6Case { assert IPv6NetworkUtils.isValidManagementEndpoint(IPV6) assert IPv6NetworkUtils.isValidManagementEndpoint("host-01.example.com") assert !IPv6NetworkUtils.isValidManagementEndpoint(LINK_LOCAL_IPV6) + assert !IPv6NetworkUtils.isValidManagementEndpoint(LOOPBACK_IPV6) assert !IPv6NetworkUtils.isValidManagementEndpoint(INVALID_IP) } void testJGroupsInitialHostsIpv6Format() { - assert Platform.formatJGroupsInitialHosts(IPV6, "2001:db8::2", JGROUP_PORT) == + assert Platform.formatJGroupsInitialHosts(IPV6, IPV6_2, JGROUP_PORT) == "[2001:db8::1][7805],[2001:db8::2][7805]" } diff --git a/test/src/test/groovy/org/zstack/test/integration/kvm/host/KvmHostIpv6Case.groovy b/test/src/test/groovy/org/zstack/test/integration/kvm/host/KvmHostIpv6Case.groovy index b2428569378..6a3def29a3c 100644 --- a/test/src/test/groovy/org/zstack/test/integration/kvm/host/KvmHostIpv6Case.groovy +++ b/test/src/test/groovy/org/zstack/test/integration/kvm/host/KvmHostIpv6Case.groovy @@ -16,9 +16,10 @@ class KvmHostIpv6Case extends SubCase { EnvSpec env ClusterInventory cluster - private static final String LOOPBACK_IPV6_FULL = "0:0:0:0:0:0:0:1" - private static final String LOOPBACK_IPV6_CANONICAL = "::1" + private static final String GLOBAL_IPV6_FULL = "2001:0db8:0000:0000:0000:0000:0000:0010" + private static final String GLOBAL_IPV6_CANONICAL = "2001:db8::10" private static final String LINK_LOCAL_IPV6 = "fe80::1" + private static final String LOOPBACK_IPV6 = "::1" private static final String INVALID_MANAGEMENT_IP = "not-an-ip!!" @Override @@ -50,15 +51,15 @@ class KvmHostIpv6Case extends SubCase { action.sessionId = adminSession() action.resourceUuid = Platform.uuid action.clusterUuid = cluster.uuid - action.managementIp = LOOPBACK_IPV6_FULL + action.managementIp = GLOBAL_IPV6_FULL action.name = "kvm-ipv6" action.username = "root" action.password = "password" def res = action.call() assert res.error == null - assert (res.value.inventory as KVMHostInventory).managementIp == LOOPBACK_IPV6_CANONICAL - assert Q.New(HostVO.class).eq(HostVO_.managementIp, LOOPBACK_IPV6_CANONICAL).isExists() + assert (res.value.inventory as KVMHostInventory).managementIp == GLOBAL_IPV6_CANONICAL + assert Q.New(HostVO.class).eq(HostVO_.managementIp, GLOBAL_IPV6_CANONICAL).isExists() } void testRejectInvalidAndLinkLocalIpv6() { @@ -78,6 +79,20 @@ class KvmHostIpv6Case extends SubCase { assert res.error.code == SysErrors.INVALID_ARGUMENT_ERROR.toString() assert Q.New(HostVO.class).count() == before + action = new AddKVMHostAction() + action.sessionId = adminSession() + action.resourceUuid = Platform.uuid + action.clusterUuid = cluster.uuid + action.managementIp = LOOPBACK_IPV6 + action.name = "kvm-loopback" + action.username = "root" + action.password = "password" + res = action.call() + + assert res.error != null + assert res.error.code == SysErrors.INVALID_ARGUMENT_ERROR.toString() + assert Q.New(HostVO.class).count() == before + action = new AddKVMHostAction() action.sessionId = adminSession() action.resourceUuid = Platform.uuid diff --git a/utils/src/main/java/org/zstack/utils/network/IPv6NetworkUtils.java b/utils/src/main/java/org/zstack/utils/network/IPv6NetworkUtils.java index beea7545e59..38d57c890b2 100644 --- a/utils/src/main/java/org/zstack/utils/network/IPv6NetworkUtils.java +++ b/utils/src/main/java/org/zstack/utils/network/IPv6NetworkUtils.java @@ -8,6 +8,8 @@ import org.zstack.utils.logging.CLogger; import java.math.BigInteger; +import java.net.Inet6Address; +import java.net.InetAddress; import java.util.Arrays; import java.util.List; @@ -541,6 +543,23 @@ public static boolean isValidManagementEndpoint(String endpoint) { return true; } - return isIpv6Address(endpoint) && !isLinkLocalAddress(endpoint); + return isValidManagementIpv6Address(endpoint); + } + + private static boolean isValidManagementIpv6Address(String endpoint) { + if (!isIpv6Address(endpoint)) { + return false; + } + + try { + InetAddress address = InetAddress.getByName(endpoint); + return address instanceof Inet6Address && + !address.isLinkLocalAddress() && + !address.isLoopbackAddress() && + !address.isAnyLocalAddress() && + !address.isMulticastAddress(); + } catch (Exception e) { + return false; + } } } From f4e2cb9d1b039df2df0de0cee017d4c1f35dff0d Mon Sep 17 00:00:00 2001 From: "shixin.ruan" Date: Thu, 21 May 2026 19:50:35 +0900 Subject: [PATCH 05/53] [mgt-ipv6]: persist mn id and rest ipv6 urls Persist managementServerId across management IP changes and add IPv6-safe REST URL helpers with focused coverage. Resolves: ZSTAC-79206 Change-Id: I03d57df9dd199aba5875f1726cb5cc1cc4b1e32e --- .../main/java/org/zstack/core/Platform.java | 68 ++++++++++++++++++- .../org/zstack/core/rest/RESTFacadeImpl.java | 38 +++++++---- .../core/ManagementNetworkIpv6Case.groovy | 44 ++++++++++++ 3 files changed, 134 insertions(+), 16 deletions(-) diff --git a/core/src/main/java/org/zstack/core/Platform.java b/core/src/main/java/org/zstack/core/Platform.java index 82bd1047042..f1eb3c3db5d 100755 --- a/core/src/main/java/org/zstack/core/Platform.java +++ b/core/src/main/java/org/zstack/core/Platform.java @@ -47,6 +47,7 @@ import java.io.File; import java.io.FileInputStream; +import java.io.FileOutputStream; import java.io.IOException; import java.lang.management.ManagementFactory; import java.lang.management.RuntimeMXBean; @@ -59,6 +60,8 @@ import java.net.NetworkInterface; import java.net.SocketException; import java.net.UnknownHostException; +import java.nio.file.Files; +import java.nio.file.StandardCopyOption; import java.sql.Timestamp; import java.util.*; import java.util.concurrent.TimeUnit; @@ -88,9 +91,12 @@ public class Platform { private static final String DEFAULT_ROUTE_MARK = "default via"; private static final String JGROUPS_INITIAL_HOST_FORMAT = "%s[%s],%s[%s]"; private static final int IP_ADDRESS_COMMAND_CIDR_INDEX = 1; + private static final String TEMP_FILE_SUFFIX = ".tmp"; + private static final String ZSTACK_UUID_PATTERN = "[0-9a-fA-F]{32}"; private static EncryptRSA rsa = new EncryptRSA(); private static Map errorCounter = new HashMap<>(); + public static final String MANAGEMENT_SERVER_ID_PROPERTY = "managementServerId"; public static final String COMPONENT_CLASSPATH_HOME = "componentsHome"; public static final String FAKE_UUID = "THIS_IS_A_FAKE_UUID"; @@ -503,8 +509,8 @@ private static void prepareHibernateSearchProperties() { in = new FileInputStream(globalPropertiesFile); System.getProperties().load(in); - // get ms ip should after global property setup - msId = UUID.nameUUIDFromBytes(getManagementServerIp().getBytes()).toString().replaceAll("-", ""); + // get ms id should after global property setup + msId = loadOrCreateManagementServerId(globalPropertiesFile, Platform::getUuid); collectDynamicObjectMetadata(); linkGlobalProperty(); @@ -711,6 +717,64 @@ public static String getManagementServerId() { return msId; } + public static synchronized String loadOrCreateManagementServerId(File propertiesFile, Supplier idSupplier) { + Properties properties = new Properties(); + if (propertiesFile.exists()) { + try (FileInputStream inputStream = new FileInputStream(propertiesFile)) { + properties.load(inputStream); + } catch (IOException e) { + throw new CloudRuntimeException(e); + } + } + + String configuredId = properties.getProperty(MANAGEMENT_SERVER_ID_PROPERTY); + if (isValidManagementServerId(configuredId)) { + System.setProperty(MANAGEMENT_SERVER_ID_PROPERTY, configuredId); + return configuredId; + } + + String generatedId = idSupplier.get(); + if (!isValidManagementServerId(generatedId)) { + throw new CloudRuntimeException(String.format("generated management server id[%s] is not a valid uuid", generatedId)); + } + + properties.setProperty(MANAGEMENT_SERVER_ID_PROPERTY, generatedId); + saveManagementServerId(propertiesFile, properties); + System.setProperty(MANAGEMENT_SERVER_ID_PROPERTY, generatedId); + return generatedId; + } + + private static boolean isValidManagementServerId(String id) { + if (id == null) { + return false; + } + + try { + UUID.fromString(id); + return true; + } catch (IllegalArgumentException ignored) { + return id.matches(ZSTACK_UUID_PATTERN); + } + } + + private static void saveManagementServerId(File propertiesFile, Properties properties) { + File tmp = new File(propertiesFile.getAbsolutePath() + TEMP_FILE_SUFFIX); + try (FileOutputStream outputStream = new FileOutputStream(tmp)) { + properties.store(outputStream, "ZStack properties"); + try { + Files.move(tmp.toPath(), propertiesFile.toPath(), StandardCopyOption.REPLACE_EXISTING, StandardCopyOption.ATOMIC_MOVE); + } catch (IOException e) { + Files.move(tmp.toPath(), propertiesFile.toPath(), StandardCopyOption.REPLACE_EXISTING); + } + } catch (IOException e) { + throw new CloudRuntimeException(e); + } finally { + if (tmp.exists()) { + tmp.delete(); + } + } + } + public static , T extends Enum> StateMachine createStateMachine() { return new StateMachineImpl(); } diff --git a/core/src/main/java/org/zstack/core/rest/RESTFacadeImpl.java b/core/src/main/java/org/zstack/core/rest/RESTFacadeImpl.java index f66be622706..41f1354ceb1 100755 --- a/core/src/main/java/org/zstack/core/rest/RESTFacadeImpl.java +++ b/core/src/main/java/org/zstack/core/rest/RESTFacadeImpl.java @@ -193,20 +193,10 @@ void init() { callbackHostName = hostname.trim(); } - String url = IPv6NetworkUtils.buildHttpUrl(callbackHostName, port); - if (path != null && !path.isEmpty()) { - url = String.format("%s/%s", url, path); - } - UriComponentsBuilder ub = UriComponentsBuilder.fromHttpUrl(url); - ub.path(RESTConstant.CALLBACK_PATH); - callbackUrl = ub.build().toUriString(); - - ub = UriComponentsBuilder.fromHttpUrl(url); - baseUrl = ub.build().toUriString(); - - ub = UriComponentsBuilder.fromHttpUrl(url); - ub.path(RESTConstant.COMMAND_CHANNEL_PATH); - sendCommandUrl = ub.build().toUriString(); + String url = buildBaseUrl(callbackHostName, port, path); + callbackUrl = buildCallbackUrl(callbackHostName, port, path); + baseUrl = url; + sendCommandUrl = buildSendCommandUrl(callbackHostName, port, path); logger.debug(String.format("RESTFacade built callback url: %s", callbackUrl)); template = RESTFacade.createRestTemplate(CoreGlobalProperty.REST_FACADE_READ_TIMEOUT, CoreGlobalProperty.REST_FACADE_CONNECT_TIMEOUT); @@ -217,6 +207,26 @@ void init() { CoreGlobalProperty.REST_FACADE_MAX_TOTAL); } + public static String buildBaseUrl(String hostName, int port, String path) { + String url = IPv6NetworkUtils.buildHttpUrl(hostName, port); + if (path != null && !path.isEmpty()) { + url = String.format("%s/%s", url, path); + } + return UriComponentsBuilder.fromHttpUrl(url).build().toUriString(); + } + + public static String buildCallbackUrl(String hostName, int port, String path) { + UriComponentsBuilder ub = UriComponentsBuilder.fromHttpUrl(buildBaseUrl(hostName, port, path)); + ub.path(RESTConstant.CALLBACK_PATH); + return ub.build().toUriString(); + } + + public static String buildSendCommandUrl(String hostName, int port, String path) { + UriComponentsBuilder ub = UriComponentsBuilder.fromHttpUrl(buildBaseUrl(hostName, port, path)); + ub.path(RESTConstant.COMMAND_CHANNEL_PATH); + return ub.build().toUriString(); + } + // timeout are in milliseconds private static AsyncRestTemplate createAsyncRestTemplate(int readTimeout, int connectTimeout, int maxPerRoute, int maxTotal) { PoolingNHttpClientConnectionManager connectionManager; diff --git a/test/src/test/groovy/org/zstack/test/integration/core/ManagementNetworkIpv6Case.groovy b/test/src/test/groovy/org/zstack/test/integration/core/ManagementNetworkIpv6Case.groovy index b02e3df7847..4916ae91dc5 100644 --- a/test/src/test/groovy/org/zstack/test/integration/core/ManagementNetworkIpv6Case.groovy +++ b/test/src/test/groovy/org/zstack/test/integration/core/ManagementNetworkIpv6Case.groovy @@ -4,11 +4,15 @@ import org.zstack.appliancevm.ApplianceVmConstant import org.zstack.appliancevm.ApplianceVmFacadeImpl import org.zstack.core.NetworkGlobalConfig import org.zstack.core.Platform +import org.zstack.core.rest.RESTFacadeImpl +import org.zstack.header.rest.RESTConstant import org.zstack.utils.network.IPv6Constants import org.zstack.utils.network.IPv6NetworkUtils import org.zstack.utils.network.NetworkUtils import org.junit.Test +import java.util.function.Supplier + class ManagementNetworkIpv6Case { private static final String IPV4 = "192.168.1.10" private static final String IPV6 = "2001:db8::1" @@ -17,6 +21,8 @@ class ManagementNetworkIpv6Case { private static final String LINK_LOCAL_IPV6 = "fe80::1" private static final String LOOPBACK_IPV6 = "::1" private static final String INVALID_IP = "not-an-ip!!" + private static final String MANAGEMENT_SERVER_ID = "1234567890abcdef1234567890abcdef" + private static final String NEW_MANAGEMENT_SERVER_ID = "abcdef1234567890abcdef1234567890" private static final int REST_PORT = 8080 private static final int JGROUP_PORT = 7805 @@ -28,6 +34,7 @@ class ManagementNetworkIpv6Case { testSelectApplianceVmManagementNodeIpByCidr() testBuildUrlIpv4() testBuildUrlIpv6() + testRestFacadeIpv6Urls() testBuildHostPortIpv6() testBracketIpv6Idempotent() testNormalizeIpv6() @@ -37,6 +44,7 @@ class ManagementNetworkIpv6Case { testIpv6NetworkCidr() testIpInCidrDualStack() testManagementCidrIpVersionOverload() + testManagementServerIdPersisted() testApplianceVmBootstrapParam() } @@ -93,6 +101,15 @@ class ManagementNetworkIpv6Case { assert IPv6NetworkUtils.buildHttpUrl(IPV6, REST_PORT) == "http://[2001:db8::1]:8080" } + void testRestFacadeIpv6Urls() { + assert RESTFacadeImpl.buildBaseUrl(IPV6, REST_PORT, null) == "http://[2001:db8::1]:8080" + assert RESTFacadeImpl.buildBaseUrl(IPV6, REST_PORT, "zstack") == "http://[2001:db8::1]:8080/zstack" + assert RESTFacadeImpl.buildCallbackUrl(IPV6, REST_PORT, "zstack") == + "http://[2001:db8::1]:8080/zstack${RESTConstant.CALLBACK_PATH}" + assert RESTFacadeImpl.buildSendCommandUrl(IPV6, REST_PORT, "zstack") == + "http://[2001:db8::1]:8080/zstack${RESTConstant.COMMAND_CHANNEL_PATH}" + } + void testBuildHostPortIpv6() { assert IPv6NetworkUtils.formatHostPort(IPV6, REST_PORT) == "[2001:db8::1]:8080" } @@ -140,6 +157,33 @@ class ManagementNetworkIpv6Case { assert Platform.getManagementServerCidr(IPv6Constants.IPv4) == Platform.getManagementServerCidr(Platform.getManagementServerIp()) } + void testManagementServerIdPersisted() { + String oldValue = System.getProperty(Platform.MANAGEMENT_SERVER_ID_PROPERTY) + File propertiesFile = File.createTempFile("zstack-management-server-id", ".properties") + try { + propertiesFile.text = "" + String generatedId = Platform.loadOrCreateManagementServerId( + propertiesFile, + { -> MANAGEMENT_SERVER_ID } as Supplier) + assert generatedId == MANAGEMENT_SERVER_ID + Properties properties = new Properties() + propertiesFile.withInputStream { properties.load(it) } + assert properties.getProperty(Platform.MANAGEMENT_SERVER_ID_PROPERTY) == MANAGEMENT_SERVER_ID + + String persistedId = Platform.loadOrCreateManagementServerId( + propertiesFile, + { -> NEW_MANAGEMENT_SERVER_ID } as Supplier) + assert persistedId == MANAGEMENT_SERVER_ID + } finally { + propertiesFile.delete() + if (oldValue == null) { + System.clearProperty(Platform.MANAGEMENT_SERVER_ID_PROPERTY) + } else { + System.setProperty(Platform.MANAGEMENT_SERVER_ID_PROPERTY, oldValue) + } + } + } + void testApplianceVmBootstrapParam() { assert ApplianceVmConstant.BootstrapParams.managementNodeIp6Cidr.toString() == "managementNodeIp6Cidr" } From 4c985a42b5e305d007b34f899df5d273ad227bc0 Mon Sep 17 00:00:00 2001 From: "shixin.ruan" Date: Thu, 21 May 2026 19:54:48 +0900 Subject: [PATCH 06/53] [mgt-ipv6]: cover cidr command parsing Add focused coverage for IPv4 and IPv6 management CIDR parsing from ip address command output. Resolves: ZSTAC-79206 Change-Id: Id3776bc971679c3e1b88f14b1f8a19ced6621098 --- .../main/java/org/zstack/core/Platform.java | 39 +++++++++++++++---- .../core/ManagementNetworkIpv6Case.groovy | 20 ++++++++++ 2 files changed, 51 insertions(+), 8 deletions(-) diff --git a/core/src/main/java/org/zstack/core/Platform.java b/core/src/main/java/org/zstack/core/Platform.java index f1eb3c3db5d..d002313271a 100755 --- a/core/src/main/java/org/zstack/core/Platform.java +++ b/core/src/main/java/org/zstack/core/Platform.java @@ -91,6 +91,8 @@ public class Platform { private static final String DEFAULT_ROUTE_MARK = "default via"; private static final String JGROUPS_INITIAL_HOST_FORMAT = "%s[%s],%s[%s]"; private static final int IP_ADDRESS_COMMAND_CIDR_INDEX = 1; + private static final int IP_ADDRESS_COMMAND_MIN_TOKEN_COUNT = 2; + private static final String CIDR_SEPARATOR = "/"; private static final String TEMP_FILE_SUFFIX = ".tmp"; private static final String ZSTACK_UUID_PATTERN = "[0-9a-fA-F]{32}"; private static EncryptRSA rsa = new EncryptRSA(); @@ -860,14 +862,35 @@ public static boolean isVIPNode() { private static String getManagementServerCidrInternal(String mgtIp) { String command = IPv6NetworkUtils.isIpv6Address(mgtIp) ? IPV6_ADDRESS_COMMAND : IPV4_ADDRESS_COMMAND; Linux.ShellResult ret = Linux.shell(command); - for (String line : ret.getStdout().split("\\n")) { - if (line.contains(mgtIp)) { - line = line.trim(); - try { - return NetworkUtils.getNetworkAddressFromCidr(line.split(" ")[IP_ADDRESS_COMMAND_CIDR_INDEX]); - } catch (RuntimeException e) { - return null; - } + return parseManagementServerCidrFromIpAddressOutput(mgtIp, ret.getStdout()); + } + + public static String parseManagementServerCidrFromIpAddressOutput(String managementIp, String commandOutput) { + if (commandOutput == null) { + return null; + } + + String normalizedManagementIp = normalizeManagementIp(managementIp); + for (String line : commandOutput.split("\\n")) { + String[] tokens = line.trim().split("\\s+"); + if (tokens.length < IP_ADDRESS_COMMAND_MIN_TOKEN_COUNT) { + continue; + } + + String cidr = tokens[IP_ADDRESS_COMMAND_CIDR_INDEX]; + if (!cidr.contains(CIDR_SEPARATOR)) { + continue; + } + + String ip = cidr.substring(0, cidr.indexOf(CIDR_SEPARATOR)); + if (!normalizedManagementIp.equals(normalizeManagementIp(ip))) { + continue; + } + + try { + return NetworkUtils.getNetworkAddressFromCidr(cidr); + } catch (RuntimeException e) { + return null; } } diff --git a/test/src/test/groovy/org/zstack/test/integration/core/ManagementNetworkIpv6Case.groovy b/test/src/test/groovy/org/zstack/test/integration/core/ManagementNetworkIpv6Case.groovy index 4916ae91dc5..0c891f3b9df 100644 --- a/test/src/test/groovy/org/zstack/test/integration/core/ManagementNetworkIpv6Case.groovy +++ b/test/src/test/groovy/org/zstack/test/integration/core/ManagementNetworkIpv6Case.groovy @@ -23,6 +23,19 @@ class ManagementNetworkIpv6Case { private static final String INVALID_IP = "not-an-ip!!" private static final String MANAGEMENT_SERVER_ID = "1234567890abcdef1234567890abcdef" private static final String NEW_MANAGEMENT_SERVER_ID = "abcdef1234567890abcdef1234567890" + private static final String IPV4_ADDRESS_COMMAND_OUTPUT = """\ +2: eth0 + inet 192.168.1.10/24 brd 192.168.1.255 scope global eth0 +3: eth1 + inet 10.0.0.10/24 brd 10.0.0.255 scope global eth1 +""" + private static final String IPV6_ADDRESS_COMMAND_OUTPUT = """\ +2: eth0 + inet6 2001:db8::1/64 scope global + valid_lft forever preferred_lft forever + inet6 fe80::1/64 scope link + valid_lft forever preferred_lft forever +""" private static final int REST_PORT = 8080 private static final int JGROUP_PORT = 7805 @@ -43,6 +56,7 @@ class ManagementNetworkIpv6Case { testJGroupsInitialHostsIpv4Regression() testIpv6NetworkCidr() testIpInCidrDualStack() + testManagementCidrCommandOutputParsing() testManagementCidrIpVersionOverload() testManagementServerIdPersisted() testApplianceVmBootstrapParam() @@ -153,6 +167,12 @@ class ManagementNetworkIpv6Case { assert !NetworkUtils.isIpInCidr(IPV6, "192.168.1.0/24") } + void testManagementCidrCommandOutputParsing() { + assert Platform.parseManagementServerCidrFromIpAddressOutput(IPV4, IPV4_ADDRESS_COMMAND_OUTPUT) == "192.168.1.0/24" + assert Platform.parseManagementServerCidrFromIpAddressOutput(IPV6, IPV6_ADDRESS_COMMAND_OUTPUT) == "2001:db8::/64" + assert Platform.parseManagementServerCidrFromIpAddressOutput(IPV6_2, IPV6_ADDRESS_COMMAND_OUTPUT) == null + } + void testManagementCidrIpVersionOverload() { assert Platform.getManagementServerCidr(IPv6Constants.IPv4) == Platform.getManagementServerCidr(Platform.getManagementServerIp()) } From 2adeb7eacab6c41f5454e33c04a3c4bc5708826e Mon Sep 17 00:00:00 2001 From: "shixin.ruan" Date: Thu, 21 May 2026 20:01:44 +0900 Subject: [PATCH 07/53] [mgt-ipv6]: cover nfs ceph vxlan ipv6 Add focused management IPv6 coverage for NFS URL parsing, Ceph mon URL parsing, and VXLAN VTEP validation. Resolves: ZSTAC-79206 Change-Id: Ia2925ec043a3b2be4d1e3b921ad285a8aebc9aed --- .../java/org/zstack/storage/ceph/MonUri.java | 11 +++++++ .../primary/nfs/NfsApiParamChecker.java | 8 ++--- .../VxlanPoolApiInterceptor.java | 8 +++-- .../core/ManagementNetworkIpv6Case.groovy | 32 +++++++++++++++++++ 4 files changed, 53 insertions(+), 6 deletions(-) diff --git a/plugin/ceph/src/main/java/org/zstack/storage/ceph/MonUri.java b/plugin/ceph/src/main/java/org/zstack/storage/ceph/MonUri.java index 7a7fdad4d57..00b89db714a 100755 --- a/plugin/ceph/src/main/java/org/zstack/storage/ceph/MonUri.java +++ b/plugin/ceph/src/main/java/org/zstack/storage/ceph/MonUri.java @@ -26,6 +26,8 @@ public class MonUri { private int sshPort = 22; private static List allowedQueryParameter; + private static final String IPV6_URL_HOST_PREFIX = "["; + private static final String IPV6_URL_HOST_SUFFIX = "]"; static { allowedQueryParameter = list("monPort"); } @@ -90,6 +92,7 @@ public MonUri(String url) { " in format of %s", url, MON_URL_FORMAT) ); } + hostname = stripIpv6Brackets(hostname); sshPort = uri.getPort() == -1 ? sshPort : uri.getPort(); if (sshPort < 1 || sshPort > 65535) { @@ -104,6 +107,14 @@ public MonUri(String url) { } } + private static String stripIpv6Brackets(String host) { + if (host != null && host.startsWith(IPV6_URL_HOST_PREFIX) && host.endsWith(IPV6_URL_HOST_SUFFIX)) { + return host.substring(IPV6_URL_HOST_PREFIX.length(), host.length() - IPV6_URL_HOST_SUFFIX.length()); + } + + return host; + } + public int getSshPort() { return sshPort; } diff --git a/plugin/nfsPrimaryStorage/src/main/java/org/zstack/storage/primary/nfs/NfsApiParamChecker.java b/plugin/nfsPrimaryStorage/src/main/java/org/zstack/storage/primary/nfs/NfsApiParamChecker.java index dca56b1bb91..da283932bbc 100755 --- a/plugin/nfsPrimaryStorage/src/main/java/org/zstack/storage/primary/nfs/NfsApiParamChecker.java +++ b/plugin/nfsPrimaryStorage/src/main/java/org/zstack/storage/primary/nfs/NfsApiParamChecker.java @@ -51,13 +51,13 @@ public void checkUrl(String zoneUuid, List systemTags, String url) { throw new ApiMessageInterceptionException(argerr(ORG_ZSTACK_STORAGE_PRIMARY_NFS_10006, "there has been a nfs primary storage having url as %s in zone[uuid:%s]", url, zoneUuid)); } - String path = getNfsPath(url); + String path = getNfsPathFromUrl(url); if (path != null && ( path.startsWith("/dev") || path.startsWith("/proc") || path.startsWith("/sys"))) { throw new ApiMessageInterceptionException(argerr(ORG_ZSTACK_STORAGE_PRIMARY_NFS_10007, " the url contains an invalid folder[/dev or /proc or /sys]")); } - validateUrl(systemTags, getNfsHost(url)); + validateUrl(systemTags, getNfsHostFromUrl(url)); } @@ -88,7 +88,7 @@ private void validateCidrTag(String sysTag, String ipAddr) { } } - private String getNfsHost(String url) { + public static String getNfsHostFromUrl(String url) { if (url.startsWith(IPV6_URL_HOST_PREFIX)) { int end = url.indexOf(IPV6_URL_HOST_SUFFIX); if (end > 0) { @@ -99,7 +99,7 @@ private String getNfsHost(String url) { return url.split(NFS_URL_SEPARATOR)[0]; } - private String getNfsPath(String url) { + public static String getNfsPathFromUrl(String url) { if (url.startsWith(IPV6_URL_HOST_PREFIX)) { int end = url.indexOf(IPV6_URL_HOST_SUFFIX); if (end > 0 && url.length() > end + IPV6_URL_HOST_SUFFIX.length()) { diff --git a/plugin/vxlan/src/main/java/org/zstack/network/l2/vxlan/vxlanNetworkPool/VxlanPoolApiInterceptor.java b/plugin/vxlan/src/main/java/org/zstack/network/l2/vxlan/vxlanNetworkPool/VxlanPoolApiInterceptor.java index 8310624e1a4..1ed33145250 100644 --- a/plugin/vxlan/src/main/java/org/zstack/network/l2/vxlan/vxlanNetworkPool/VxlanPoolApiInterceptor.java +++ b/plugin/vxlan/src/main/java/org/zstack/network/l2/vxlan/vxlanNetworkPool/VxlanPoolApiInterceptor.java @@ -56,7 +56,7 @@ public APIMessage intercept(APIMessage msg) throws ApiMessageInterceptionExcepti } private void validate(APICreateVxlanPoolRemoteVtepMsg msg) { - if (!NetworkUtils.isIpAddress(msg.getRemoteVtepIp())) { + if (!isValidVtepIp(msg.getRemoteVtepIp())) { throw new ApiMessageInterceptionException(argerr(ORG_ZSTACK_NETWORK_L2_VXLAN_VXLANNETWORKPOOL_10032, "%s is not a valid IP address", msg.getRemoteVtepIp())); } @@ -72,12 +72,16 @@ private void validate(APICreateVxlanPoolRemoteVtepMsg msg) { } private void validate(APIDeleteVxlanPoolRemoteVtepMsg msg) { - if (!NetworkUtils.isIpAddress(msg.getRemoteVtepIp())) { + if (!isValidVtepIp(msg.getRemoteVtepIp())) { throw new ApiMessageInterceptionException(argerr(ORG_ZSTACK_NETWORK_L2_VXLAN_VXLANNETWORKPOOL_10033, "%s is not a valid IP address", msg.getRemoteVtepIp())); } } + public static boolean isValidVtepIp(String vtepIp) { + return NetworkUtils.isIpAddress(vtepIp); + } + private void validate(APICreateVxlanVtepMsg msg) { long count = Q.New(VtepVO.class).eq(VtepVO_.hostUuid, msg.getHostUuid()).eq(VtepVO_.poolUuid, msg.getPoolUuid()).count(); if (count > 0) { diff --git a/test/src/test/groovy/org/zstack/test/integration/core/ManagementNetworkIpv6Case.groovy b/test/src/test/groovy/org/zstack/test/integration/core/ManagementNetworkIpv6Case.groovy index 0c891f3b9df..a50969d62f1 100644 --- a/test/src/test/groovy/org/zstack/test/integration/core/ManagementNetworkIpv6Case.groovy +++ b/test/src/test/groovy/org/zstack/test/integration/core/ManagementNetworkIpv6Case.groovy @@ -6,6 +6,9 @@ import org.zstack.core.NetworkGlobalConfig import org.zstack.core.Platform import org.zstack.core.rest.RESTFacadeImpl import org.zstack.header.rest.RESTConstant +import org.zstack.network.l2.vxlan.vxlanNetworkPool.VxlanPoolApiInterceptor +import org.zstack.storage.ceph.MonUri +import org.zstack.storage.primary.nfs.NfsApiParamChecker import org.zstack.utils.network.IPv6Constants import org.zstack.utils.network.IPv6NetworkUtils import org.zstack.utils.network.NetworkUtils @@ -23,6 +26,11 @@ class ManagementNetworkIpv6Case { private static final String INVALID_IP = "not-an-ip!!" private static final String MANAGEMENT_SERVER_ID = "1234567890abcdef1234567890abcdef" private static final String NEW_MANAGEMENT_SERVER_ID = "abcdef1234567890abcdef1234567890" + private static final String NFS_EXPORT_PATH = "/export/nfs" + private static final String NFS_IPV4_URL = "${IPV4}:${NFS_EXPORT_PATH}" + private static final String NFS_IPV6_URL = "[${IPV6}]:${NFS_EXPORT_PATH}" + private static final String CEPH_IPV6_MON_URL = "root:password@[${IPV6}]:22/?monPort=6789" + private static final String INVALID_VTEP_IP = "not-a-vtep-ip" private static final String IPV4_ADDRESS_COMMAND_OUTPUT = """\ 2: eth0 inet 192.168.1.10/24 brd 192.168.1.255 scope global eth0 @@ -59,6 +67,9 @@ class ManagementNetworkIpv6Case { testManagementCidrCommandOutputParsing() testManagementCidrIpVersionOverload() testManagementServerIdPersisted() + testNfsIpv6UrlParsing() + testCephIpv6MonUrlParsing() + testVxlanVtepIpv6Validation() testApplianceVmBootstrapParam() } @@ -204,6 +215,27 @@ class ManagementNetworkIpv6Case { } } + void testNfsIpv6UrlParsing() { + assert NfsApiParamChecker.getNfsHostFromUrl(NFS_IPV4_URL) == IPV4 + assert NfsApiParamChecker.getNfsPathFromUrl(NFS_IPV4_URL) == NFS_EXPORT_PATH + assert NfsApiParamChecker.getNfsHostFromUrl(NFS_IPV6_URL) == IPV6 + assert NfsApiParamChecker.getNfsPathFromUrl(NFS_IPV6_URL) == NFS_EXPORT_PATH + } + + void testCephIpv6MonUrlParsing() { + MonUri monUri = new MonUri(CEPH_IPV6_MON_URL) + assert monUri.hostname == IPV6 + assert monUri.sshPort == 22 + assert monUri.monPort == 6789 + assert IPv6NetworkUtils.formatHostPort(monUri.hostname, monUri.monPort) == "[${IPV6}]:6789" + } + + void testVxlanVtepIpv6Validation() { + assert VxlanPoolApiInterceptor.isValidVtepIp(IPV4) + assert VxlanPoolApiInterceptor.isValidVtepIp(IPV6) + assert !VxlanPoolApiInterceptor.isValidVtepIp(INVALID_VTEP_IP) + } + void testApplianceVmBootstrapParam() { assert ApplianceVmConstant.BootstrapParams.managementNodeIp6Cidr.toString() == "managementNodeIp6Cidr" } From 77a4839978f11673d0f82b6b8890938dcebfadb5 Mon Sep 17 00:00:00 2001 From: "shixin.ruan" Date: Thu, 21 May 2026 20:06:37 +0900 Subject: [PATCH 08/53] [mgt-ipv6]: cover kvm extra ip cidr Add focused IPv6 coverage for KVM extra IP CIDR selection used by data and migration network paths. Resolves: ZSTAC-79206 Change-Id: I03ea5987d0e4ebc4fdd78dc261297baaf12d0832 --- .../src/main/java/org/zstack/kvm/KVMHost.java | 27 ++++++++++--------- .../core/ManagementNetworkIpv6Case.groovy | 9 +++++++ 2 files changed, 24 insertions(+), 12 deletions(-) diff --git a/plugin/kvm/src/main/java/org/zstack/kvm/KVMHost.java b/plugin/kvm/src/main/java/org/zstack/kvm/KVMHost.java index 59ce9aad88b..aeb796970ae 100755 --- a/plugin/kvm/src/main/java/org/zstack/kvm/KVMHost.java +++ b/plugin/kvm/src/main/java/org/zstack/kvm/KVMHost.java @@ -126,6 +126,7 @@ public class KVMHost extends HostBase implements Host { private static final CLogger logger = Utils.getLogger(KVMHost.class); private static final ZTester tester = Utils.getTester(); + private static final String EXTRA_IP_SEPARATOR = ","; protected static OperationChecker allowedOperations = new OperationChecker(true); protected static OperationChecker skipOperations = new OperationChecker(true); @@ -2205,10 +2206,19 @@ protected static String getDataNetworkAddress(String hostUuid, String cidr) { return null; } - final String[] ips = extraIps.split(","); - for (String ip: ips) { - if (NetworkUtils.isIpInCidr(ip, cidr)) { - return ip; + return selectIpInCidr(extraIps, cidr); + } + + public static String selectIpInCidr(String ips, String cidr) { + if (ips == null) { + return null; + } + + final String[] ipList = ips.split(EXTRA_IP_SEPARATOR); + for (String ip: ipList) { + String trimmedIp = ip.trim(); + if (NetworkUtils.isIpInCidr(trimmedIp, cidr)) { + return trimmedIp; } } @@ -6790,14 +6800,7 @@ private boolean checkMigrateNetworkCidrOfHost(String cidr) { return false; } - final String[] ips = extraIps.split(","); - for (String ip: ips) { - if (NetworkUtils.isIpInCidr(ip, cidr)) { - return true; - } - } - - return false; + return selectIpInCidr(extraIps, cidr) != null; } private boolean checkQemuLibvirtVersionOfHost() { diff --git a/test/src/test/groovy/org/zstack/test/integration/core/ManagementNetworkIpv6Case.groovy b/test/src/test/groovy/org/zstack/test/integration/core/ManagementNetworkIpv6Case.groovy index a50969d62f1..822c5a6ed4d 100644 --- a/test/src/test/groovy/org/zstack/test/integration/core/ManagementNetworkIpv6Case.groovy +++ b/test/src/test/groovy/org/zstack/test/integration/core/ManagementNetworkIpv6Case.groovy @@ -6,6 +6,7 @@ import org.zstack.core.NetworkGlobalConfig import org.zstack.core.Platform import org.zstack.core.rest.RESTFacadeImpl import org.zstack.header.rest.RESTConstant +import org.zstack.kvm.KVMHost import org.zstack.network.l2.vxlan.vxlanNetworkPool.VxlanPoolApiInterceptor import org.zstack.storage.ceph.MonUri import org.zstack.storage.primary.nfs.NfsApiParamChecker @@ -31,6 +32,7 @@ class ManagementNetworkIpv6Case { private static final String NFS_IPV6_URL = "[${IPV6}]:${NFS_EXPORT_PATH}" private static final String CEPH_IPV6_MON_URL = "root:password@[${IPV6}]:22/?monPort=6789" private static final String INVALID_VTEP_IP = "not-a-vtep-ip" + private static final String HOST_EXTRA_IPS = "10.0.0.10,${IPV6_2}" private static final String IPV4_ADDRESS_COMMAND_OUTPUT = """\ 2: eth0 inet 192.168.1.10/24 brd 192.168.1.255 scope global eth0 @@ -70,6 +72,7 @@ class ManagementNetworkIpv6Case { testNfsIpv6UrlParsing() testCephIpv6MonUrlParsing() testVxlanVtepIpv6Validation() + testKvmExtraIpCidrSelection() testApplianceVmBootstrapParam() } @@ -236,6 +239,12 @@ class ManagementNetworkIpv6Case { assert !VxlanPoolApiInterceptor.isValidVtepIp(INVALID_VTEP_IP) } + void testKvmExtraIpCidrSelection() { + assert KVMHost.selectIpInCidr(HOST_EXTRA_IPS, "10.0.0.0/24") == "10.0.0.10" + assert KVMHost.selectIpInCidr(HOST_EXTRA_IPS, "2001:db8::/64") == IPV6_2 + assert KVMHost.selectIpInCidr(HOST_EXTRA_IPS, "172.16.0.0/16") == null + } + void testApplianceVmBootstrapParam() { assert ApplianceVmConstant.BootstrapParams.managementNodeIp6Cidr.toString() == "managementNodeIp6Cidr" } From c57ba6e03c400d3f864437aa309d6d4a42fd9ed5 Mon Sep 17 00:00:00 2001 From: "shixin.ruan" Date: Thu, 21 May 2026 20:47:10 +0900 Subject: [PATCH 09/53] [mgt-ipv6]: cover vr bootstrap json Add focused ApplianceVm IPv6 bootstrap JSON tests for MN IP and CIDR fields. Resolves: ZSTAC-79206 Change-Id: Icdbe40c69af9306eb2663a12031774c1b4c64195 --- .../appliancevm/ApplianceVmFacadeImpl.java | 29 ++++++--- .../appliancevm/ApplianceVmIpv6Case.groovy | 63 +++++++++++++++++++ 2 files changed, 84 insertions(+), 8 deletions(-) create mode 100644 test/src/test/groovy/org/zstack/test/integration/appliancevm/ApplianceVmIpv6Case.groovy diff --git a/plugin/applianceVm/src/main/java/org/zstack/appliancevm/ApplianceVmFacadeImpl.java b/plugin/applianceVm/src/main/java/org/zstack/appliancevm/ApplianceVmFacadeImpl.java index 2c8b3e3c1d9..99159d643bf 100755 --- a/plugin/applianceVm/src/main/java/org/zstack/appliancevm/ApplianceVmFacadeImpl.java +++ b/plugin/applianceVm/src/main/java/org/zstack/appliancevm/ApplianceVmFacadeImpl.java @@ -466,16 +466,13 @@ public Map prepareBootstrapInformation(VmInstanceSpec spec) { String publicKey = asf.getPublicKey(); ret.put(ApplianceVmConstant.BootstrapParams.publicKey.toString(), publicKey); ret.put(BootstrapParams.uuid.toString(), spec.getVmInventory().getUuid()); - ret.put(BootstrapParams.managementNodeIp.toString(), selectManagementNodeIpForBootstrap( + putManagementNodeBootstrapParams(ret, Platform.getManagementServerIps(), getVrManagementCidrs(mgmtNic), - Platform.getManagementServerIp())); - ret.put(BootstrapParams.managementNodeVip.toString(), Platform.getManagementServerVip()); - ret.put(BootstrapParams.managementNodeCidr.toString(), Platform.getManagementServerCidr()); - String managementNodeIp6Cidr = Platform.getManagementServerIp6Cidr(); - if (managementNodeIp6Cidr != null) { - ret.put(BootstrapParams.managementNodeIp6Cidr.toString(), managementNodeIp6Cidr); - } + Platform.getManagementServerIp(), + Platform.getManagementServerVip(), + Platform.getManagementServerCidr(), + Platform.getManagementServerIp6Cidr()); /* this is only used by ApplianceVmPrepareBootstrapInfoExtensionPoint extension point, will be deleted after extension point */ ret.put(BootstrapParams.additionalL3Uuids.toString(), additionalNics.stream().map(VmNicInventory::getL3NetworkUuid).collect(Collectors.toList())); @@ -508,6 +505,22 @@ public static String selectManagementNodeIpForBootstrap(Collection mnIps return fallbackIp; } + public static void putManagementNodeBootstrapParams(Map ret, + Collection mnIps, + Collection vrManagementCidrs, + String fallbackIp, + String managementNodeVip, + String managementNodeCidr, + String managementNodeIp6Cidr) { + ret.put(BootstrapParams.managementNodeIp.toString(), + selectManagementNodeIpForBootstrap(mnIps, vrManagementCidrs, fallbackIp)); + ret.put(BootstrapParams.managementNodeVip.toString(), managementNodeVip); + ret.put(BootstrapParams.managementNodeCidr.toString(), managementNodeCidr); + if (managementNodeIp6Cidr != null) { + ret.put(BootstrapParams.managementNodeIp6Cidr.toString(), managementNodeIp6Cidr); + } + } + private Collection getVrManagementCidrs(VmNicInventory managementNic) { List cidrs = new ArrayList<>(); if (managementNic == null || managementNic.getUsedIps() == null) { diff --git a/test/src/test/groovy/org/zstack/test/integration/appliancevm/ApplianceVmIpv6Case.groovy b/test/src/test/groovy/org/zstack/test/integration/appliancevm/ApplianceVmIpv6Case.groovy new file mode 100644 index 00000000000..194b0ac5e9e --- /dev/null +++ b/test/src/test/groovy/org/zstack/test/integration/appliancevm/ApplianceVmIpv6Case.groovy @@ -0,0 +1,63 @@ +package org.zstack.test.integration.appliancevm + +import org.junit.Test +import org.zstack.appliancevm.ApplianceVmConstant +import org.zstack.appliancevm.ApplianceVmFacadeImpl + +class ApplianceVmIpv6Case { + private static final String IPV4_MN_IP = "192.168.1.10" + private static final String IPV6_MN_IP = "2001:db8::1" + private static final String IPV4_MN_CIDR = "192.168.1.0/24" + private static final String IPV6_MN_CIDR = "2001:db8::/64" + private static final String UNMATCHED_VR_CIDR = "10.0.0.0/24" + + @Test + void testVrBootstrapIpv6Cidr() { + Map params = buildDualStackBootstrapParams([IPV6_MN_CIDR]) + + assert params.get(ApplianceVmConstant.BootstrapParams.managementNodeIp6Cidr.toString()) == IPV6_MN_CIDR + } + + @Test + void testVrBootstrapMnIpNoBrackets() { + Map params = buildDualStackBootstrapParams([IPV6_MN_CIDR]) + + assert params.get(ApplianceVmConstant.BootstrapParams.managementNodeIp.toString()) == IPV6_MN_IP + } + + @Test + void testVrMnIpCidrMatch() { + Map params = buildDualStackBootstrapParams([IPV6_MN_CIDR]) + + assert params.get(ApplianceVmConstant.BootstrapParams.managementNodeIp.toString()) == IPV6_MN_IP + } + + @Test + void testVrBootstrapAddressFamilyIndependent() { + Map params = buildDualStackBootstrapParams([IPV6_MN_CIDR]) + + assert params.get(ApplianceVmConstant.BootstrapParams.managementNodeIp.toString()) == IPV6_MN_IP + assert params.get(ApplianceVmConstant.BootstrapParams.managementNodeCidr.toString()) == IPV4_MN_CIDR + assert params.get(ApplianceVmConstant.BootstrapParams.managementNodeIp6Cidr.toString()) == IPV6_MN_CIDR + } + + @Test + void testVrBootstrapFallbackWhenNoCidrMatches() { + Map params = buildDualStackBootstrapParams([UNMATCHED_VR_CIDR]) + + assert params.get(ApplianceVmConstant.BootstrapParams.managementNodeIp.toString()) == IPV4_MN_IP + } + + private static Map buildDualStackBootstrapParams(Collection vrManagementCidrs) { + Map params = [:] + ApplianceVmFacadeImpl.putManagementNodeBootstrapParams( + params, + [IPV4_MN_IP, IPV6_MN_IP], + vrManagementCidrs, + IPV4_MN_IP, + null, + IPV4_MN_CIDR, + IPV6_MN_CIDR) + return params + } +} From 7e9556e7813761bf61213ecb4a4df3defe9b8c10 Mon Sep 17 00:00:00 2001 From: "shixin.ruan" Date: Thu, 21 May 2026 21:30:00 +0900 Subject: [PATCH 10/53] [mgt-ipv6]: avoid allocator lambda verify error Avoid lambda bytecode that blocks KvmHostIpv6Case initialization before IPv6 assertions. Resolves: ZSTAC-79206 Change-Id: I2d1fdced05d1127f6414e4ae0adc491e10b23d7d --- .../compute/allocator/HostAllocatorManagerImpl.java | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/compute/src/main/java/org/zstack/compute/allocator/HostAllocatorManagerImpl.java b/compute/src/main/java/org/zstack/compute/allocator/HostAllocatorManagerImpl.java index a6598082004..2f59c6fb49f 100755 --- a/compute/src/main/java/org/zstack/compute/allocator/HostAllocatorManagerImpl.java +++ b/compute/src/main/java/org/zstack/compute/allocator/HostAllocatorManagerImpl.java @@ -237,11 +237,15 @@ public String call(HostUsedCpuMem arg) { } }); - hostUuids.stream().filter(huuid -> !hostHasVms.contains(huuid)).forEach(huuid -> { + for (String huuid : hostUuids) { + if (hostHasVms.contains(huuid)) { + continue; + } + HostUsedCpuMem s = new HostUsedCpuMem(); s.hostUuid = huuid; hostUsedCpuMemList.add(s); - }); + } for (final HostUsedCpuMem s : hostUsedCpuMemList) { new HostCapacityUpdater(s.hostUuid).run(new HostCapacityUpdaterRunnable() { From 29bb7c24f09c6aca68186af144c69efa2b41e68d Mon Sep 17 00:00:00 2001 From: "shixin.ruan" Date: Thu, 21 May 2026 21:36:29 +0900 Subject: [PATCH 11/53] [mgt-ipv6]: cover host ipv6 validation Add focused coverage for host management IPv6 canonicalization and invalid endpoint detection without starting the full KVM Spring harness. Resolves: ZSTAC-79206 Change-Id: I731c2168a22e5b52f993319f21835ca9a456a76b --- .../compute/host/HostApiInterceptor.java | 6 ++++- .../host/HostApiInterceptorIpv6Case.groovy | 26 +++++++++++++++++++ 2 files changed, 31 insertions(+), 1 deletion(-) create mode 100644 test/src/test/groovy/org/zstack/compute/host/HostApiInterceptorIpv6Case.groovy diff --git a/compute/src/main/java/org/zstack/compute/host/HostApiInterceptor.java b/compute/src/main/java/org/zstack/compute/host/HostApiInterceptor.java index 0cb297076be..405193cbc2d 100755 --- a/compute/src/main/java/org/zstack/compute/host/HostApiInterceptor.java +++ b/compute/src/main/java/org/zstack/compute/host/HostApiInterceptor.java @@ -125,6 +125,10 @@ private void validate(APIUpdateHostMsg msg) { } private void validate(APIAddHostMsg msg) { + validateManagementEndpoint(msg); + } + + static void validateManagementEndpoint(APIAddHostMsg msg) { if (!isValidManagementEndpoint(msg.getManagementIp())) { throw new ApiMessageInterceptionException(argerr(ORG_ZSTACK_COMPUTE_HOST_10128, INVALID_MANAGEMENT_IP_ERROR, msg.getManagementIp())); } @@ -134,7 +138,7 @@ private void validate(APIAddHostMsg msg) { } } - private boolean isValidManagementEndpoint(String endpoint) { + private static boolean isValidManagementEndpoint(String endpoint) { return IPv6NetworkUtils.isValidManagementEndpoint(endpoint); } diff --git a/test/src/test/groovy/org/zstack/compute/host/HostApiInterceptorIpv6Case.groovy b/test/src/test/groovy/org/zstack/compute/host/HostApiInterceptorIpv6Case.groovy new file mode 100644 index 00000000000..dce6e0796ef --- /dev/null +++ b/test/src/test/groovy/org/zstack/compute/host/HostApiInterceptorIpv6Case.groovy @@ -0,0 +1,26 @@ +package org.zstack.compute.host + +import org.junit.Test +import org.zstack.kvm.APIAddKVMHostMsg +import org.zstack.utils.network.IPv6NetworkUtils + +class HostApiInterceptorIpv6Case { + private static final String GLOBAL_IPV6_FULL = "2001:0db8:0000:0000:0000:0000:0000:0010" + private static final String GLOBAL_IPV6_CANONICAL = "2001:db8::10" + private static final String LINK_LOCAL_IPV6 = "fe80::1" + private static final String LOOPBACK_IPV6 = "::1" + private static final String INVALID_MANAGEMENT_IP = "not-an-ip!!" + + @Test + void testAddHostManagementIpv6CanonicalizationAndInvalidEndpointDetection() { + APIAddKVMHostMsg msg = new APIAddKVMHostMsg() + msg.managementIp = GLOBAL_IPV6_FULL + + HostApiInterceptor.validateManagementEndpoint(msg) + + assert msg.managementIp == GLOBAL_IPV6_CANONICAL + assert !IPv6NetworkUtils.isValidManagementEndpoint(INVALID_MANAGEMENT_IP) + assert !IPv6NetworkUtils.isValidManagementEndpoint(LOOPBACK_IPV6) + assert !IPv6NetworkUtils.isValidManagementEndpoint(LINK_LOCAL_IPV6) + } +} From 3abd6869316ff28ecdd6d762cd705bb9a05b3fef Mon Sep 17 00:00:00 2001 From: "shixin.ruan" Date: Thu, 21 May 2026 21:55:33 +0900 Subject: [PATCH 12/53] [mgt-ipv6]: cover vxlan ipv6 remote vtep Add API and DB assertions for IPv6 remote VXLAN VTEP creation and invalid remote VTEP rejection. Resolves: ZSTAC-79206 Change-Id: Ieb2368b5f4a274bcad5b192003ac051ae4179552 --- .../AddRemoteVxlanVtepIpCase.groovy | 37 ++++++++++++++++--- 1 file changed, 32 insertions(+), 5 deletions(-) diff --git a/test/src/test/groovy/org/zstack/test/integration/network/vxlanNetwork/AddRemoteVxlanVtepIpCase.groovy b/test/src/test/groovy/org/zstack/test/integration/network/vxlanNetwork/AddRemoteVxlanVtepIpCase.groovy index 5802f180415..034d607567d 100644 --- a/test/src/test/groovy/org/zstack/test/integration/network/vxlanNetwork/AddRemoteVxlanVtepIpCase.groovy +++ b/test/src/test/groovy/org/zstack/test/integration/network/vxlanNetwork/AddRemoteVxlanVtepIpCase.groovy @@ -18,6 +18,10 @@ import org.zstack.sdk.ApiException class AddRemoteVxlanVtepIpCase extends SubCase { + private static final String IPV4_REMOTE_VTEP_IP = "1.1.1.1" + private static final String IPV6_REMOTE_VTEP_IP = "2001:db8:ffff::10" + private static final String INVALID_REMOTE_VTEP_IP = "not-a-vtep-ip" + EnvSpec env @Override @@ -182,7 +186,7 @@ class AddRemoteVxlanVtepIpCase extends SubCase { createVxlanPoolRemoteVtep { l2NetworkUuid = pool.uuid clusterUuid = cluster.uuid - remoteVtepIp = "1.1.1.1" + remoteVtepIp = IPV4_REMOTE_VTEP_IP } assert Q.New(RemoteVtepVO.class).eq(RemoteVtepVO_.poolUuid, pool.uuid).isExists() assert Q.New(RemoteVtepVO.class).eq(RemoteVtepVO_.poolUuid, pool.uuid).eq(RemoteVtepVO_.clusterUuid,cluster.uuid).isExists() @@ -192,7 +196,7 @@ class AddRemoteVxlanVtepIpCase extends SubCase { createVxlanPoolRemoteVtep { l2NetworkUuid = pool.uuid clusterUuid = cluster2.uuid - remoteVtepIp = "1.1.1.1" + remoteVtepIp = IPV4_REMOTE_VTEP_IP } assert Q.New(RemoteVtepVO.class).eq(RemoteVtepVO_.poolUuid, pool.uuid).isExists() assert Q.New(RemoteVtepVO.class).eq(RemoteVtepVO_.poolUuid, pool.uuid).eq(RemoteVtepVO_.clusterUuid,cluster2.uuid).isExists() @@ -202,14 +206,14 @@ class AddRemoteVxlanVtepIpCase extends SubCase { createVxlanPoolRemoteVtep { l2NetworkUuid = pool.uuid clusterUuid = cluster2.uuid - remoteVtepIp = "1.1.1.1" + remoteVtepIp = IPV4_REMOTE_VTEP_IP } } deleteVxlanPoolRemoteVtep { l2NetworkUuid = pool.uuid clusterUuid = cluster.uuid - remoteVtepIp = "1.1.1.1" + remoteVtepIp = IPV4_REMOTE_VTEP_IP } assert !Q.New(RemoteVtepVO.class).eq(RemoteVtepVO_.poolUuid, pool.uuid).eq(RemoteVtepVO_.clusterUuid,cluster.uuid).isExists() assert Q.New(RemoteVtepVO.class).eq(RemoteVtepVO_.poolUuid, pool.uuid).count() == 1 @@ -217,9 +221,32 @@ class AddRemoteVxlanVtepIpCase extends SubCase { deleteVxlanPoolRemoteVtep { l2NetworkUuid = pool.uuid clusterUuid = cluster2.uuid - remoteVtepIp = "1.1.1.1" + remoteVtepIp = IPV4_REMOTE_VTEP_IP } assert !Q.New(RemoteVtepVO.class).eq(RemoteVtepVO_.poolUuid, pool.uuid).isExists() + createVxlanPoolRemoteVtep { + l2NetworkUuid = pool.uuid + clusterUuid = cluster.uuid + remoteVtepIp = IPV6_REMOTE_VTEP_IP + } + assert Q.New(RemoteVtepVO.class) + .eq(RemoteVtepVO_.poolUuid, pool.uuid) + .eq(RemoteVtepVO_.clusterUuid, cluster.uuid) + .eq(RemoteVtepVO_.vtepIp, IPV6_REMOTE_VTEP_IP) + .isExists() + assert Q.New(RemoteVtepVO.class).eq(RemoteVtepVO_.poolUuid, pool.uuid).count() == 1 + + expect(AssertionError.class) { + createVxlanPoolRemoteVtep { + l2NetworkUuid = pool.uuid + clusterUuid = cluster2.uuid + remoteVtepIp = INVALID_REMOTE_VTEP_IP + } + } + assert !Q.New(RemoteVtepVO.class) + .eq(RemoteVtepVO_.poolUuid, pool.uuid) + .eq(RemoteVtepVO_.vtepIp, INVALID_REMOTE_VTEP_IP) + .isExists() } } From 8457962b5315f92a7e77e05931b20542aef5930e Mon Sep 17 00:00:00 2001 From: "shixin.ruan" Date: Thu, 21 May 2026 22:53:53 +0900 Subject: [PATCH 13/53] [mgt-ipv6]: wire global config event facade Avoid relying only on Configurable injection for GlobalConfig instances created during GlobalConfigFacadeImpl startup. Resolves: ZSTAC-79206 Change-Id: I3f975fc23240d88ba08dc870220277cc065f7cfb --- core/src/main/java/org/zstack/core/config/GlobalConfig.java | 5 +++++ .../java/org/zstack/core/config/GlobalConfigFacadeImpl.java | 4 ++++ 2 files changed, 9 insertions(+) diff --git a/core/src/main/java/org/zstack/core/config/GlobalConfig.java b/core/src/main/java/org/zstack/core/config/GlobalConfig.java index 32b377c995b..fcb30c9664c 100755 --- a/core/src/main/java/org/zstack/core/config/GlobalConfig.java +++ b/core/src/main/java/org/zstack/core/config/GlobalConfig.java @@ -78,6 +78,11 @@ public EventFacade getEvtf() { return evtf; } + void wire(DatabaseFacade dbf, EventFacade evtf) { + this.dbf = dbf; + this.evtf = evtf; + } + @Override public String toString() { return JSONObjectUtil.toJsonString(map( diff --git a/core/src/main/java/org/zstack/core/config/GlobalConfigFacadeImpl.java b/core/src/main/java/org/zstack/core/config/GlobalConfigFacadeImpl.java index 2e551e9c0d7..0ff57c0ec2a 100755 --- a/core/src/main/java/org/zstack/core/config/GlobalConfigFacadeImpl.java +++ b/core/src/main/java/org/zstack/core/config/GlobalConfigFacadeImpl.java @@ -4,6 +4,7 @@ import org.zstack.core.asyncbatch.While; import org.zstack.core.cloudbus.CloudBus; import org.zstack.core.cloudbus.CloudBusCallBack; +import org.zstack.core.cloudbus.EventFacade; import org.zstack.core.cloudbus.MessageSafe; import org.zstack.core.componentloader.PluginRegistry; import org.zstack.core.db.DatabaseFacade; @@ -55,6 +56,8 @@ public class GlobalConfigFacadeImpl extends AbstractService implements GlobalCon @Autowired private DatabaseFacade dbf; @Autowired + private EventFacade evtf; + @Autowired private ErrorFacade errf; @Autowired private PluginRegistry pluginRgty; @@ -336,6 +339,7 @@ private void loadConfigFromJava() { private void initAllConfig() { for (GlobalConfig config : configsFromXml.values()) { + config.wire(dbf, evtf); config.init(); } } From ffb60f20cb77e99bd6838a210b64e81f29543c6c Mon Sep 17 00:00:00 2001 From: "shixin.ruan" Date: Fri, 22 May 2026 00:33:06 +0900 Subject: [PATCH 14/53] [mgt-ipv6]: cover host and vtep ipv6 Make startup-only objects use explicit facades instead of AspectJ injection during unit tests. Move allocator and VM flow extension lookup to runtime to avoid early plugin registry access. Allow IPv6 remote VTEP addresses through API and SDK validation and cover host/VTEP IPv6 cases. Verification: - mvn -Dtest=KvmHostIpv6Case test - mvn -Dtest=AddRemoteVxlanVtepIpCase test - ./runMavenProfile premium Change-Id: I0146cfab99160e1f784170d051444e4fec69ad83 Related: ZSTAC-79206 --- .../compute/allocator/TagAllocatorFlow.java | 3 +- .../compute/vm/VmCreateOnHypervisorFlow.java | 4 +- ...InstantiateResourceForChangeImageFlow.java | 8 +- .../vm/VmInstantiateResourcePostFlow.java | 8 +- .../vm/VmInstantiateResourcePreFlow.java | 8 +- .../compute/vm/VmReleaseResourceFlow.java | 7 +- .../compute/vm/VmStartOnHypervisorFlow.java | 4 +- .../core/config/GlobalConfigConstant.java | 2 + .../core/config/GlobalConfigFacadeImpl.java | 2 +- .../zstack/core/db/DatabaseFacadeImpl.java | 2 +- .../main/java/org/zstack/core/db/GLock.java | 7 ++ .../org/zstack/core/db/SimpleQueryImpl.java | 6 ++ .../core/timeout/ApiTimeoutManagerImpl.java | 11 ++- .../APICreateVxlanPoolRemoteVtepMsg.java | 2 +- .../APIDeleteVxlanPoolRemoteVtepMsg.java | 2 +- .../VxlanNetworkPoolConstant.java | 1 + .../zstack/resourceconfig/ResourceConfig.java | 5 ++ .../ResourceConfigFacadeImpl.java | 8 +- .../sdk/CreateVxlanPoolRemoteVtepAction.java | 4 +- .../sdk/DeleteVxlanPoolRemoteVtepAction.java | 4 +- .../PrimaryStorageFeatureAllocatorFlow.java | 4 +- .../PrimaryStorageTagAllocatorFlow.java | 7 +- .../kvm/host/KvmHostIpv6Case.groovy | 21 +++++ .../AddRemoteVxlanVtepIpCase.groovy | 86 ------------------- 24 files changed, 94 insertions(+), 122 deletions(-) diff --git a/compute/src/main/java/org/zstack/compute/allocator/TagAllocatorFlow.java b/compute/src/main/java/org/zstack/compute/allocator/TagAllocatorFlow.java index 0d046d2cc7e..c98bb3403bf 100755 --- a/compute/src/main/java/org/zstack/compute/allocator/TagAllocatorFlow.java +++ b/compute/src/main/java/org/zstack/compute/allocator/TagAllocatorFlow.java @@ -40,7 +40,7 @@ public class TagAllocatorFlow extends AbstractHostAllocatorFlow { private List instanceOfferingExtensions; private List diskOfferingExtensions; - public TagAllocatorFlow() { + private void loadExtensions() { instanceOfferingExtensions = pluginRgty.getExtensionList(InstanceOfferingTagAllocatorExtensionPoint.class); diskOfferingExtensions = pluginRgty.getExtensionList(DiskOfferingTagAllocatorExtensionPoint.class); } @@ -48,6 +48,7 @@ public TagAllocatorFlow() { @Override public void allocate() { throwExceptionIfIAmTheFirstFlow(); + loadExtensions(); if (!instanceOfferingExtensions.isEmpty()) { SimpleQuery q = dbf.createQuery(SystemTagVO.class); diff --git a/compute/src/main/java/org/zstack/compute/vm/VmCreateOnHypervisorFlow.java b/compute/src/main/java/org/zstack/compute/vm/VmCreateOnHypervisorFlow.java index 5b054dede75..6a40c15c34b 100755 --- a/compute/src/main/java/org/zstack/compute/vm/VmCreateOnHypervisorFlow.java +++ b/compute/src/main/java/org/zstack/compute/vm/VmCreateOnHypervisorFlow.java @@ -33,10 +33,8 @@ public class VmCreateOnHypervisorFlow implements Flow { @Autowired private EventFacade evtf; - private final List exts = pluginRgty.getExtensionList(VmBeforeCreateOnHypervisorExtensionPoint.class); - private void fireExtensions(VmInstanceSpec spec) { - for (VmBeforeCreateOnHypervisorExtensionPoint ext : exts) { + for (VmBeforeCreateOnHypervisorExtensionPoint ext : pluginRgty.getExtensionList(VmBeforeCreateOnHypervisorExtensionPoint.class)) { ext.beforeCreateVmOnHypervisor(spec); } } diff --git a/compute/src/main/java/org/zstack/compute/vm/VmInstantiateResourceForChangeImageFlow.java b/compute/src/main/java/org/zstack/compute/vm/VmInstantiateResourceForChangeImageFlow.java index bbdb2978036..dbf1b574e35 100755 --- a/compute/src/main/java/org/zstack/compute/vm/VmInstantiateResourceForChangeImageFlow.java +++ b/compute/src/main/java/org/zstack/compute/vm/VmInstantiateResourceForChangeImageFlow.java @@ -25,8 +25,9 @@ public class VmInstantiateResourceForChangeImageFlow implements Flow { @Autowired private PluginRegistry pluginRgty; - private final List extensions = pluginRgty.getExtensionList(ChangeVmImageExtensionPoint.class); - + private List getExtensions() { + return pluginRgty.getExtensionList(ChangeVmImageExtensionPoint.class); + } private void runExtensions(final Iterator it, final VmInstanceSpec spec, final FlowTrigger chain) { if (!it.hasNext()) { @@ -53,6 +54,7 @@ public void fail(ErrorCode errorCode) { @Override public void run(FlowTrigger chain, Map data) { VmInstanceSpec spec = (VmInstanceSpec) data.get(VmInstanceConstant.Params.VmInstanceSpec.toString()); + List extensions = getExtensions(); for (ChangeVmImageExtensionPoint extp : extensions) { try { extp.preBeforeInstantiateVmResource(spec); @@ -89,6 +91,6 @@ public void fail(ErrorCode errorCode) { @Override public void rollback(FlowRollback chain, Map data) { VmInstanceSpec spec = (VmInstanceSpec) data.get(VmInstanceConstant.Params.VmInstanceSpec.toString()); - rollbackExtensions(extensions.iterator(), spec, chain); + rollbackExtensions(getExtensions().iterator(), spec, chain); } } diff --git a/compute/src/main/java/org/zstack/compute/vm/VmInstantiateResourcePostFlow.java b/compute/src/main/java/org/zstack/compute/vm/VmInstantiateResourcePostFlow.java index e6a7f14c34d..a5f8e46bd54 100755 --- a/compute/src/main/java/org/zstack/compute/vm/VmInstantiateResourcePostFlow.java +++ b/compute/src/main/java/org/zstack/compute/vm/VmInstantiateResourcePostFlow.java @@ -28,11 +28,13 @@ public class VmInstantiateResourcePostFlow implements Flow { @Autowired private PluginRegistry pluginRgty; - private final List extensions = pluginRgty.getExtensionList(PostVmInstantiateResourceExtensionPoint.class); - + private List getExtensions() { + return pluginRgty.getExtensionList(PostVmInstantiateResourceExtensionPoint.class); + } public void run(FlowTrigger trigger, Map data) { VmInstanceSpec spec = (VmInstanceSpec) data.get(VmInstanceConstant.Params.VmInstanceSpec.toString()); + List extensions = getExtensions(); for (PostVmInstantiateResourceExtensionPoint ext : extensions) { ext.postBeforeInstantiateVmResource(spec); } @@ -64,7 +66,7 @@ public void fail(ErrorCode errorCode) { @Override public void rollback(FlowRollback trigger, Map data) { VmInstanceSpec spec = (VmInstanceSpec) data.get(VmInstanceConstant.Params.VmInstanceSpec.toString()); - rollbackExtensions(extensions.iterator(), spec, trigger); + rollbackExtensions(getExtensions().iterator(), spec, trigger); } private void rollbackExtensions(final Iterator iterator, final VmInstanceSpec spec, final FlowRollback trigger) { diff --git a/compute/src/main/java/org/zstack/compute/vm/VmInstantiateResourcePreFlow.java b/compute/src/main/java/org/zstack/compute/vm/VmInstantiateResourcePreFlow.java index 4c7b6eee38f..4f4f7e6ff63 100755 --- a/compute/src/main/java/org/zstack/compute/vm/VmInstantiateResourcePreFlow.java +++ b/compute/src/main/java/org/zstack/compute/vm/VmInstantiateResourcePreFlow.java @@ -29,8 +29,9 @@ public class VmInstantiateResourcePreFlow implements Flow { @Autowired private PluginRegistry pluginRgty; - private final List extensions = pluginRgty.getExtensionList(PreVmInstantiateResourceExtensionPoint.class); - + private List getExtensions() { + return pluginRgty.getExtensionList(PreVmInstantiateResourceExtensionPoint.class); + } private void runExtensions(final Iterator it, final VmInstanceSpec spec, final FlowTrigger chain) { if (!it.hasNext()) { @@ -61,6 +62,7 @@ public void fail(ErrorCode errorCode) { @Override public void run(FlowTrigger chain, Map data) { VmInstanceSpec spec = (VmInstanceSpec) data.get(VmInstanceConstant.Params.VmInstanceSpec.toString()); + List extensions = getExtensions(); for (PreVmInstantiateResourceExtensionPoint extp : extensions) { try { extp.preBeforeInstantiateVmResource(spec); @@ -97,6 +99,6 @@ public void fail(ErrorCode errorCode) { @Override public void rollback(FlowRollback chain, Map data) { VmInstanceSpec spec = (VmInstanceSpec) data.get(VmInstanceConstant.Params.VmInstanceSpec.toString()); - rollbackExtensions(extensions.iterator(), spec, chain); + rollbackExtensions(getExtensions().iterator(), spec, chain); } } diff --git a/compute/src/main/java/org/zstack/compute/vm/VmReleaseResourceFlow.java b/compute/src/main/java/org/zstack/compute/vm/VmReleaseResourceFlow.java index a57b93acde1..119ffdb74f5 100755 --- a/compute/src/main/java/org/zstack/compute/vm/VmReleaseResourceFlow.java +++ b/compute/src/main/java/org/zstack/compute/vm/VmReleaseResourceFlow.java @@ -26,9 +26,10 @@ public class VmReleaseResourceFlow implements Flow { @Autowired private PluginRegistry pluginRgty; - - private final List extensions = pluginRgty.getExtensionList(VmReleaseResourceExtensionPoint.class); + private List getExtensions() { + return pluginRgty.getExtensionList(VmReleaseResourceExtensionPoint.class); + } private void fireExtensions(final Iterator it, final VmInstanceSpec spec, final Map ctx, final FlowTrigger chain) { if (!it.hasNext()) { @@ -53,7 +54,7 @@ public void fail(ErrorCode errorCode) { @Override public void run(FlowTrigger chain, Map data) { VmInstanceSpec spec = (VmInstanceSpec) data.get(VmInstanceConstant.Params.VmInstanceSpec.toString()); - fireExtensions(extensions.iterator(), spec, data, chain); + fireExtensions(getExtensions().iterator(), spec, data, chain); } @Override diff --git a/compute/src/main/java/org/zstack/compute/vm/VmStartOnHypervisorFlow.java b/compute/src/main/java/org/zstack/compute/vm/VmStartOnHypervisorFlow.java index 20965f61655..291f79a0f88 100755 --- a/compute/src/main/java/org/zstack/compute/vm/VmStartOnHypervisorFlow.java +++ b/compute/src/main/java/org/zstack/compute/vm/VmStartOnHypervisorFlow.java @@ -26,10 +26,8 @@ public class VmStartOnHypervisorFlow implements Flow { @Autowired private PluginRegistry pluginRgty; - private final List exts = pluginRgty.getExtensionList(VmBeforeStartOnHypervisorExtensionPoint.class);; - private void fireExtensions(VmInstanceSpec spec) { - for (VmBeforeStartOnHypervisorExtensionPoint ext : exts) { + for (VmBeforeStartOnHypervisorExtensionPoint ext : pluginRgty.getExtensionList(VmBeforeStartOnHypervisorExtensionPoint.class)) { ext.beforeStartVmOnHypervisor(spec); } } diff --git a/core/src/main/java/org/zstack/core/config/GlobalConfigConstant.java b/core/src/main/java/org/zstack/core/config/GlobalConfigConstant.java index 5de3292fb30..6173958b6f4 100755 --- a/core/src/main/java/org/zstack/core/config/GlobalConfigConstant.java +++ b/core/src/main/java/org/zstack/core/config/GlobalConfigConstant.java @@ -4,4 +4,6 @@ public interface GlobalConfigConstant { public static final String SERVICE_ID = "globalConfig"; public static final String LOCK = "GlobalFacade.lock"; + + public static final long LOCK_TIMEOUT_SECONDS = 320; } diff --git a/core/src/main/java/org/zstack/core/config/GlobalConfigFacadeImpl.java b/core/src/main/java/org/zstack/core/config/GlobalConfigFacadeImpl.java index 0ff57c0ec2a..3a0fec257cb 100755 --- a/core/src/main/java/org/zstack/core/config/GlobalConfigFacadeImpl.java +++ b/core/src/main/java/org/zstack/core/config/GlobalConfigFacadeImpl.java @@ -233,7 +233,7 @@ class GlobalConfigInitializer { Map propertiesMap = new HashMap<>(); void init() { - GLock lock = new GLock(GlobalConfigConstant.LOCK, 320); + GLock lock = new GLock(GlobalConfigConstant.LOCK, GlobalConfigConstant.LOCK_TIMEOUT_SECONDS, dbf); lock.lock(); try { loadSystemProperties(); diff --git a/core/src/main/java/org/zstack/core/db/DatabaseFacadeImpl.java b/core/src/main/java/org/zstack/core/db/DatabaseFacadeImpl.java index 5a7b0c34c31..458aa6b20ba 100755 --- a/core/src/main/java/org/zstack/core/db/DatabaseFacadeImpl.java +++ b/core/src/main/java/org/zstack/core/db/DatabaseFacadeImpl.java @@ -522,7 +522,7 @@ public CriteriaBuilder getCriteriaBuilder() { public SimpleQuery createQuery(Class entityClass) { assert entityClass.isAnnotationPresent(Entity.class) : entityClass.getName() + " is not annotated by JPA @Entity"; - return new SimpleQueryImpl(entityClass); + return new SimpleQueryImpl(entityClass, this); } public EntityManager getEntityManager() { diff --git a/core/src/main/java/org/zstack/core/db/GLock.java b/core/src/main/java/org/zstack/core/db/GLock.java index a1e034d2f04..95e6f68e636 100755 --- a/core/src/main/java/org/zstack/core/db/GLock.java +++ b/core/src/main/java/org/zstack/core/db/GLock.java @@ -53,6 +53,13 @@ public GLock(String name, long timeout) { dataSource = dbf.getDataSource(); } + public GLock(String name, long timeout, DatabaseFacade dbf) { + this.name = name; + this.timeout = timeout; + this.dbf = dbf; + dataSource = dbf.getDataSource(); + } + public boolean isAlsoUseMemoryLock() { return alsoUseMemoryLock; } diff --git a/core/src/main/java/org/zstack/core/db/SimpleQueryImpl.java b/core/src/main/java/org/zstack/core/db/SimpleQueryImpl.java index 9ba9b0ac494..f65e9eae31e 100755 --- a/core/src/main/java/org/zstack/core/db/SimpleQueryImpl.java +++ b/core/src/main/java/org/zstack/core/db/SimpleQueryImpl.java @@ -76,6 +76,12 @@ class AttrInfo { _builder = _dbf.getCriteriaBuilder(); } + SimpleQueryImpl(Class vo, DatabaseFacade dbf) { + _entityClass = vo; + _dbf = dbf; + _builder = _dbf.getCriteriaBuilder(); + } + @Override public SimpleQuery select(SingularAttribute... attrs) { for (int i=0; i prepareTimeoutGlobalConfig() { private String getTimeoutGlobalConfigValue(Class clz, GlobalConfigVO vo) { // once global config already exists, use its default value as // auto generated value and do not use legacy timeout - if (Q.New(GlobalConfigVO.class) - .eq(GlobalConfigVO_.category, vo.getCategory()) - .eq(GlobalConfigVO_.name, vo.getName()).isExists()) { + if (dbf.createQuery(GlobalConfigVO.class) + .add(GlobalConfigVO_.category, SimpleQuery.Op.EQ, vo.getCategory()) + .add(GlobalConfigVO_.name, SimpleQuery.Op.EQ, vo.getName()).isExists()) { return vo.getDefaultValue(); } diff --git a/plugin/vxlan/src/main/java/org/zstack/network/l2/vxlan/vxlanNetworkPool/APICreateVxlanPoolRemoteVtepMsg.java b/plugin/vxlan/src/main/java/org/zstack/network/l2/vxlan/vxlanNetworkPool/APICreateVxlanPoolRemoteVtepMsg.java index 56343f9f2c7..03ab96e1741 100644 --- a/plugin/vxlan/src/main/java/org/zstack/network/l2/vxlan/vxlanNetworkPool/APICreateVxlanPoolRemoteVtepMsg.java +++ b/plugin/vxlan/src/main/java/org/zstack/network/l2/vxlan/vxlanNetworkPool/APICreateVxlanPoolRemoteVtepMsg.java @@ -27,7 +27,7 @@ public class APICreateVxlanPoolRemoteVtepMsg extends APICreateMessage implements @APIParam private String clusterUuid; - @APIParam(maxLength = 15) + @APIParam(maxLength = VxlanNetworkPoolConstant.REMOTE_VTEP_IP_MAX_LENGTH) private String remoteVtepIp; @Override diff --git a/plugin/vxlan/src/main/java/org/zstack/network/l2/vxlan/vxlanNetworkPool/APIDeleteVxlanPoolRemoteVtepMsg.java b/plugin/vxlan/src/main/java/org/zstack/network/l2/vxlan/vxlanNetworkPool/APIDeleteVxlanPoolRemoteVtepMsg.java index 23a8c6320dd..ea353db8ecc 100644 --- a/plugin/vxlan/src/main/java/org/zstack/network/l2/vxlan/vxlanNetworkPool/APIDeleteVxlanPoolRemoteVtepMsg.java +++ b/plugin/vxlan/src/main/java/org/zstack/network/l2/vxlan/vxlanNetworkPool/APIDeleteVxlanPoolRemoteVtepMsg.java @@ -26,7 +26,7 @@ public class APIDeleteVxlanPoolRemoteVtepMsg extends APIDeleteMessage implements @APIParam private String clusterUuid; - @APIParam(maxLength = 15) + @APIParam(maxLength = VxlanNetworkPoolConstant.REMOTE_VTEP_IP_MAX_LENGTH) private String remoteVtepIp; @Override diff --git a/plugin/vxlan/src/main/java/org/zstack/network/l2/vxlan/vxlanNetworkPool/VxlanNetworkPoolConstant.java b/plugin/vxlan/src/main/java/org/zstack/network/l2/vxlan/vxlanNetworkPool/VxlanNetworkPoolConstant.java index 9a3cc65656d..c4991de3096 100644 --- a/plugin/vxlan/src/main/java/org/zstack/network/l2/vxlan/vxlanNetworkPool/VxlanNetworkPoolConstant.java +++ b/plugin/vxlan/src/main/java/org/zstack/network/l2/vxlan/vxlanNetworkPool/VxlanNetworkPoolConstant.java @@ -15,6 +15,7 @@ public class VxlanNetworkPoolConstant { public static final Integer VXLAN_PORT = 8472; @PythonClass public static final String KVM_VXLAN_TYPE = "KVM_HOST_VXLAN"; + public static final int REMOTE_VTEP_IP_MAX_LENGTH = 39; public static final String ACTION_CATEGORY = "vxlan"; diff --git a/resourceconfig/src/main/java/org/zstack/resourceconfig/ResourceConfig.java b/resourceconfig/src/main/java/org/zstack/resourceconfig/ResourceConfig.java index 276ca6bd35d..48edde25600 100644 --- a/resourceconfig/src/main/java/org/zstack/resourceconfig/ResourceConfig.java +++ b/resourceconfig/src/main/java/org/zstack/resourceconfig/ResourceConfig.java @@ -57,6 +57,11 @@ public static ResourceConfig valueOf(GlobalConfig globalConfig, BindResourceConf return result; } + void wire(DatabaseFacade dbf, EventFacade evtf) { + this.dbf = dbf; + this.evtf = evtf; + } + public void installLocalUpdateExtension(ResourceConfigUpdateExtensionPoint ext) { localUpdateExtensions.add(ext); } diff --git a/resourceconfig/src/main/java/org/zstack/resourceconfig/ResourceConfigFacadeImpl.java b/resourceconfig/src/main/java/org/zstack/resourceconfig/ResourceConfigFacadeImpl.java index db3c696392c..668e96f74c4 100644 --- a/resourceconfig/src/main/java/org/zstack/resourceconfig/ResourceConfigFacadeImpl.java +++ b/resourceconfig/src/main/java/org/zstack/resourceconfig/ResourceConfigFacadeImpl.java @@ -2,6 +2,7 @@ import org.springframework.beans.factory.annotation.Autowired; import org.zstack.core.cloudbus.CloudBus; +import org.zstack.core.cloudbus.EventFacade; import org.zstack.core.cloudbus.MessageSafe; import org.zstack.core.config.GlobalConfig; import org.zstack.core.config.GlobalConfigException; @@ -30,6 +31,8 @@ public class ResourceConfigFacadeImpl extends AbstractService implements Resourc @Autowired private DatabaseFacade dbf; @Autowired + private EventFacade evtf; + @Autowired private GlobalConfigFacade gcf; protected Map resourceConfigs = new HashMap<>(); @@ -240,6 +243,9 @@ protected void buildResourceConfig(Field field) throws Exception { } private void initResourceConfig() { - resourceConfigs.values().forEach(ResourceConfig::init); + resourceConfigs.values().forEach(config -> { + config.wire(dbf, evtf); + config.init(); + }); } } diff --git a/sdk/src/main/java/org/zstack/sdk/CreateVxlanPoolRemoteVtepAction.java b/sdk/src/main/java/org/zstack/sdk/CreateVxlanPoolRemoteVtepAction.java index 1c36ba12518..3aa0aa1b208 100644 --- a/sdk/src/main/java/org/zstack/sdk/CreateVxlanPoolRemoteVtepAction.java +++ b/sdk/src/main/java/org/zstack/sdk/CreateVxlanPoolRemoteVtepAction.java @@ -6,6 +6,8 @@ public class CreateVxlanPoolRemoteVtepAction extends AbstractAction { + private static final int REMOTE_VTEP_IP_MAX_LENGTH = 39; + private static final HashMap parameterMap = new HashMap<>(); private static final HashMap nonAPIParameterMap = new HashMap<>(); @@ -31,7 +33,7 @@ public Result throwExceptionIfError() { @Param(required = true, nonempty = false, nullElements = false, emptyString = true, noTrim = false) public java.lang.String clusterUuid; - @Param(required = true, maxLength = 15, nonempty = false, nullElements = false, emptyString = true, noTrim = false) + @Param(required = true, maxLength = REMOTE_VTEP_IP_MAX_LENGTH, nonempty = false, nullElements = false, emptyString = true, noTrim = false) public java.lang.String remoteVtepIp; @Param(required = false) diff --git a/sdk/src/main/java/org/zstack/sdk/DeleteVxlanPoolRemoteVtepAction.java b/sdk/src/main/java/org/zstack/sdk/DeleteVxlanPoolRemoteVtepAction.java index 610726bc31b..dd2c210c24b 100644 --- a/sdk/src/main/java/org/zstack/sdk/DeleteVxlanPoolRemoteVtepAction.java +++ b/sdk/src/main/java/org/zstack/sdk/DeleteVxlanPoolRemoteVtepAction.java @@ -6,6 +6,8 @@ public class DeleteVxlanPoolRemoteVtepAction extends AbstractAction { + private static final int REMOTE_VTEP_IP_MAX_LENGTH = 39; + private static final HashMap parameterMap = new HashMap<>(); private static final HashMap nonAPIParameterMap = new HashMap<>(); @@ -31,7 +33,7 @@ public Result throwExceptionIfError() { @Param(required = true, nonempty = false, nullElements = false, emptyString = true, noTrim = false) public java.lang.String clusterUuid; - @Param(required = true, maxLength = 15, nonempty = false, nullElements = false, emptyString = true, noTrim = false) + @Param(required = true, maxLength = REMOTE_VTEP_IP_MAX_LENGTH, nonempty = false, nullElements = false, emptyString = true, noTrim = false) public java.lang.String remoteVtepIp; @Param(required = false) diff --git a/storage/src/main/java/org/zstack/storage/primary/PrimaryStorageFeatureAllocatorFlow.java b/storage/src/main/java/org/zstack/storage/primary/PrimaryStorageFeatureAllocatorFlow.java index 226c269f9f2..6abaf247855 100644 --- a/storage/src/main/java/org/zstack/storage/primary/PrimaryStorageFeatureAllocatorFlow.java +++ b/storage/src/main/java/org/zstack/storage/primary/PrimaryStorageFeatureAllocatorFlow.java @@ -27,14 +27,12 @@ public class PrimaryStorageFeatureAllocatorFlow extends NoRollbackFlow { @Autowired private PluginRegistry pluginRgty; - protected final List featureExtensions = pluginRgty.getExtensionList(PrimaryStorageFeatureAllocatorExtensionPoint.class);; - @Override public void run(FlowTrigger trigger, Map data) { PrimaryStorageAllocationSpec spec = (PrimaryStorageAllocationSpec) data.get(PrimaryStorageConstant.AllocatorParams.SPEC); List candidates = (List) data.get(PrimaryStorageConstant.AllocatorParams.CANDIDATES); List ret; - for (PrimaryStorageFeatureAllocatorExtensionPoint extp : featureExtensions) { + for (PrimaryStorageFeatureAllocatorExtensionPoint extp : pluginRgty.getExtensionList(PrimaryStorageFeatureAllocatorExtensionPoint.class)) { ret = extp.allocatePrimaryStorage(spec.getRequiredFeatures(), candidates); if (ret == null) { continue; diff --git a/storage/src/main/java/org/zstack/storage/primary/PrimaryStorageTagAllocatorFlow.java b/storage/src/main/java/org/zstack/storage/primary/PrimaryStorageTagAllocatorFlow.java index eb65bd7e051..3444e5aa74e 100755 --- a/storage/src/main/java/org/zstack/storage/primary/PrimaryStorageTagAllocatorFlow.java +++ b/storage/src/main/java/org/zstack/storage/primary/PrimaryStorageTagAllocatorFlow.java @@ -39,8 +39,9 @@ public class PrimaryStorageTagAllocatorFlow extends NoRollbackFlow { @Autowired private PluginRegistry pluginRgty; - protected final List tagExtensions = pluginRgty.getExtensionList(PrimaryStorageTagAllocatorExtensionPoint.class);; - + protected List getTagExtensions() { + return pluginRgty.getExtensionList(PrimaryStorageTagAllocatorExtensionPoint.class); + } @Override public void run(FlowTrigger trigger, Map data) { @@ -71,7 +72,7 @@ public void run(FlowTrigger trigger, Map data) { protected List callTagExtensions(List tags, List candidates) { List ret; - for (PrimaryStorageTagAllocatorExtensionPoint extp : tagExtensions) { + for (PrimaryStorageTagAllocatorExtensionPoint extp : getTagExtensions()) { ret = extp.allocatePrimaryStorage(tags, candidates); if (ret == null) { continue; diff --git a/test/src/test/groovy/org/zstack/test/integration/kvm/host/KvmHostIpv6Case.groovy b/test/src/test/groovy/org/zstack/test/integration/kvm/host/KvmHostIpv6Case.groovy index 6a3def29a3c..5c4df94fb66 100644 --- a/test/src/test/groovy/org/zstack/test/integration/kvm/host/KvmHostIpv6Case.groovy +++ b/test/src/test/groovy/org/zstack/test/integration/kvm/host/KvmHostIpv6Case.groovy @@ -1,10 +1,16 @@ package org.zstack.test.integration.kvm.host import org.zstack.core.Platform +import org.zstack.core.cloudbus.CloudBus +import org.zstack.core.db.DatabaseFacade import org.zstack.core.db.Q import org.zstack.header.errorcode.SysErrors +import org.zstack.header.host.ConnectHostMsg +import org.zstack.header.host.ConnectHostReply +import org.zstack.header.host.CpuArchitecture import org.zstack.header.host.HostVO import org.zstack.header.host.HostVO_ +import org.zstack.kvm.KVMHostVO import org.zstack.sdk.AddKVMHostAction import org.zstack.sdk.ClusterInventory import org.zstack.sdk.KVMHostInventory @@ -15,12 +21,16 @@ import org.zstack.testlib.SubCase class KvmHostIpv6Case extends SubCase { EnvSpec env ClusterInventory cluster + DatabaseFacade dbf private static final String GLOBAL_IPV6_FULL = "2001:0db8:0000:0000:0000:0000:0000:0010" private static final String GLOBAL_IPV6_CANONICAL = "2001:db8::10" private static final String LINK_LOCAL_IPV6 = "fe80::1" private static final String LOOPBACK_IPV6 = "::1" private static final String INVALID_MANAGEMENT_IP = "not-an-ip!!" + private static final String OS_DISTRIBUTION = "centos" + private static final String OS_RELEASE = "core" + private static final String OS_VERSION = "7.6.1810" @Override void setup() { @@ -40,6 +50,7 @@ class KvmHostIpv6Case extends SubCase { @Override void test() { env.create { + dbf = bean(DatabaseFacade.class) cluster = env.inventoryByName("cluster") as ClusterInventory testAddHostWithIpv6() testRejectInvalidAndLinkLocalIpv6() @@ -47,6 +58,16 @@ class KvmHostIpv6Case extends SubCase { } void testAddHostWithIpv6() { + env.message(ConnectHostMsg.class) { ConnectHostMsg msg, CloudBus bus -> + KVMHostVO host = dbf.findByUuid(msg.uuid, KVMHostVO.class) + host.setArchitecture(CpuArchitecture.x86_64.name()) + host.setOsDistribution(OS_DISTRIBUTION) + host.setOsRelease(OS_RELEASE) + host.setOsVersion(OS_VERSION) + dbf.update(host) + bus.reply(msg, new ConnectHostReply()) + } + def action = new AddKVMHostAction() action.sessionId = adminSession() action.resourceUuid = Platform.uuid diff --git a/test/src/test/groovy/org/zstack/test/integration/network/vxlanNetwork/AddRemoteVxlanVtepIpCase.groovy b/test/src/test/groovy/org/zstack/test/integration/network/vxlanNetwork/AddRemoteVxlanVtepIpCase.groovy index 034d607567d..2e38c1ed1d5 100644 --- a/test/src/test/groovy/org/zstack/test/integration/network/vxlanNetwork/AddRemoteVxlanVtepIpCase.groovy +++ b/test/src/test/groovy/org/zstack/test/integration/network/vxlanNetwork/AddRemoteVxlanVtepIpCase.groovy @@ -32,30 +32,6 @@ class AddRemoteVxlanVtepIpCase extends SubCase { @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" - } - - image { - name = "vr" - url = "http://zstack.org/download/vr.qcow2" - } - } - zone { name = "zone" description = "test" @@ -63,70 +39,12 @@ class AddRemoteVxlanVtepIpCase extends SubCase { cluster { name = "cluster1" hypervisorType = "KVM" - - kvm { - name = "kvm1" - managementIp = "localhost" - username = "root" - password = "password" - - totalCpu = 8 - totalMem = SizeUnit.GIGABYTE.toByte(20) - } - - kvm { - name = "kvm2" - managementIp = "127.0.0.1" - username = "root" - password = "password" - - totalCpu = 8 - totalMem = SizeUnit.GIGABYTE.toByte(20) - } - - attachPrimaryStorage("local") - } cluster { name = "cluster2" hypervisorType = "KVM" - - kvm { - name = "kvm3" - managementIp = "127.0.0.2" - username = "root" - password = "password" - - totalCpu = 8 - totalMem = SizeUnit.GIGABYTE.toByte(20) - } - - kvm { - name = "kvm4" - managementIp = "127.0.0.3" - username = "root" - password = "password" - - totalCpu = 8 - totalMem = SizeUnit.GIGABYTE.toByte(20) - } - - attachPrimaryStorage("nfs-ps") - - } - - localPrimaryStorage { - name = "local" - url = "/local_ps" } - - nfsPrimaryStorage { - name = "nfs-ps" - url = "localhost:/nfs" - } - - attachBackupStorage("sftp") } } } @@ -147,10 +65,6 @@ class AddRemoteVxlanVtepIpCase extends SubCase { def zone = env.inventoryByName("zone") as ZoneInventory def cluster = env.inventoryByName("cluster1") as ClusterInventory def cluster2 = env.inventoryByName("cluster2") as ClusterInventory - def host1 = env.inventoryByName("kvm1") as KVMHostInventory - def host2 = env.inventoryByName("kvm2") as KVMHostInventory - def host3 = env.inventoryByName("kvm3") as KVMHostInventory - def host4 = env.inventoryByName("kvm4") as KVMHostInventory def pool = createL2VxlanNetworkPool { name = "TestVxlanPool1" From bd5be4466b259b3d0c999c05b7886b23719fab9a Mon Sep 17 00:00:00 2001 From: "shixin.ruan" Date: Fri, 22 May 2026 09:38:44 +0900 Subject: [PATCH 15/53] [mgt-ipv6]: split host ip validation errors Split invalid host management endpoint and reserved IPv6 validation into separate global error codes. Wire Q.New with an explicit DatabaseFacade to avoid startup-time AspectJ injection dependency during tests. Verification: - ./runMavenProfile premium - mvnTest -Dtest=HostApiInterceptorIpv6Case - mvnTest -Dtest=KvmHostIpv6Case Change-Id: I1d575fca0d2fdbb5f6574fc60c7e0ac7a8a2096a Related: ZSTAC-79206 --- .../compute/host/HostApiInterceptor.java | 25 ++++++++++++++++--- core/src/main/java/org/zstack/core/db/Q.java | 4 ++- .../host/HostApiInterceptorIpv6Case.groovy | 14 ++++++++--- .../CloudOperationsErrorCode.java | 2 ++ .../utils/network/IPv6NetworkUtils.java | 2 +- 5 files changed, 37 insertions(+), 10 deletions(-) diff --git a/compute/src/main/java/org/zstack/compute/host/HostApiInterceptor.java b/compute/src/main/java/org/zstack/compute/host/HostApiInterceptor.java index 405193cbc2d..e645f7a7bcd 100755 --- a/compute/src/main/java/org/zstack/compute/host/HostApiInterceptor.java +++ b/compute/src/main/java/org/zstack/compute/host/HostApiInterceptor.java @@ -31,6 +31,8 @@ public class HostApiInterceptor implements ApiMessageInterceptor { private static final String INVALID_MANAGEMENT_IP_ERROR = "managementIp[%s] is not a valid IPv4 address, IPv6 address, or hostname"; + private static final String RESERVED_MANAGEMENT_IPV6_ERROR = + "managementIp[%s] is an IPv6 address that cannot be used as a management address"; @Autowired private CloudBus bus; @@ -129,13 +131,28 @@ private void validate(APIAddHostMsg msg) { } static void validateManagementEndpoint(APIAddHostMsg msg) { - if (!isValidManagementEndpoint(msg.getManagementIp())) { - throw new ApiMessageInterceptionException(argerr(ORG_ZSTACK_COMPUTE_HOST_10128, INVALID_MANAGEMENT_IP_ERROR, msg.getManagementIp())); + String managementIp = msg.getManagementIp(); + String errorCode = getManagementEndpointValidationErrorCode(managementIp); + + if (errorCode != null) { + throw new ApiMessageInterceptionException(argerr(errorCode, getManagementEndpointValidationErrorMessage(errorCode), managementIp)); + } + + if (IPv6NetworkUtils.isIpv6Address(managementIp)) { + msg.setManagementIp(IPv6NetworkUtils.getIpv6AddressCanonicalString(managementIp)); } + } - if (IPv6NetworkUtils.isIpv6Address(msg.getManagementIp())) { - msg.setManagementIp(IPv6NetworkUtils.getIpv6AddressCanonicalString(msg.getManagementIp())); + static String getManagementEndpointValidationErrorCode(String managementIp) { + if (IPv6NetworkUtils.isIpv6Address(managementIp)) { + return IPv6NetworkUtils.isValidManagementIpv6Address(managementIp) ? null : ORG_ZSTACK_COMPUTE_HOST_10129; } + + return isValidManagementEndpoint(managementIp) ? null : ORG_ZSTACK_COMPUTE_HOST_10128; + } + + private static String getManagementEndpointValidationErrorMessage(String errorCode) { + return ORG_ZSTACK_COMPUTE_HOST_10129.equals(errorCode) ? RESERVED_MANAGEMENT_IPV6_ERROR : INVALID_MANAGEMENT_IP_ERROR; } private static boolean isValidManagementEndpoint(String endpoint) { diff --git a/core/src/main/java/org/zstack/core/db/Q.java b/core/src/main/java/org/zstack/core/db/Q.java index 8dcdcdf48fa..cf7682c0e85 100755 --- a/core/src/main/java/org/zstack/core/db/Q.java +++ b/core/src/main/java/org/zstack/core/db/Q.java @@ -2,6 +2,7 @@ import org.apache.commons.collections.CollectionUtils; import org.springframework.transaction.annotation.Transactional; +import org.zstack.core.Platform; import org.zstack.utils.DebugUtils; import javax.persistence.Tuple; @@ -18,7 +19,8 @@ public class Q { @SuppressWarnings("unchecked") private Q(Class clz) { - q = new SimpleQueryImpl(clz); + DatabaseFacade dbf = Platform.getComponentLoader().getComponent(DatabaseFacade.class); + q = new SimpleQueryImpl(clz, dbf); } public Q select(SingularAttribute... attrs) { diff --git a/test/src/test/groovy/org/zstack/compute/host/HostApiInterceptorIpv6Case.groovy b/test/src/test/groovy/org/zstack/compute/host/HostApiInterceptorIpv6Case.groovy index dce6e0796ef..c0aff417602 100644 --- a/test/src/test/groovy/org/zstack/compute/host/HostApiInterceptorIpv6Case.groovy +++ b/test/src/test/groovy/org/zstack/compute/host/HostApiInterceptorIpv6Case.groovy @@ -2,13 +2,17 @@ package org.zstack.compute.host import org.junit.Test import org.zstack.kvm.APIAddKVMHostMsg -import org.zstack.utils.network.IPv6NetworkUtils + +import static org.zstack.utils.clouderrorcode.CloudOperationsErrorCode.ORG_ZSTACK_COMPUTE_HOST_10128 +import static org.zstack.utils.clouderrorcode.CloudOperationsErrorCode.ORG_ZSTACK_COMPUTE_HOST_10129 class HostApiInterceptorIpv6Case { private static final String GLOBAL_IPV6_FULL = "2001:0db8:0000:0000:0000:0000:0000:0010" private static final String GLOBAL_IPV6_CANONICAL = "2001:db8::10" private static final String LINK_LOCAL_IPV6 = "fe80::1" private static final String LOOPBACK_IPV6 = "::1" + private static final String ANY_LOCAL_IPV6 = "::" + private static final String MULTICAST_IPV6 = "ff02::1" private static final String INVALID_MANAGEMENT_IP = "not-an-ip!!" @Test @@ -19,8 +23,10 @@ class HostApiInterceptorIpv6Case { HostApiInterceptor.validateManagementEndpoint(msg) assert msg.managementIp == GLOBAL_IPV6_CANONICAL - assert !IPv6NetworkUtils.isValidManagementEndpoint(INVALID_MANAGEMENT_IP) - assert !IPv6NetworkUtils.isValidManagementEndpoint(LOOPBACK_IPV6) - assert !IPv6NetworkUtils.isValidManagementEndpoint(LINK_LOCAL_IPV6) + assert HostApiInterceptor.getManagementEndpointValidationErrorCode(INVALID_MANAGEMENT_IP) == ORG_ZSTACK_COMPUTE_HOST_10128 + assert HostApiInterceptor.getManagementEndpointValidationErrorCode(LOOPBACK_IPV6) == ORG_ZSTACK_COMPUTE_HOST_10129 + assert HostApiInterceptor.getManagementEndpointValidationErrorCode(LINK_LOCAL_IPV6) == ORG_ZSTACK_COMPUTE_HOST_10129 + assert HostApiInterceptor.getManagementEndpointValidationErrorCode(ANY_LOCAL_IPV6) == ORG_ZSTACK_COMPUTE_HOST_10129 + assert HostApiInterceptor.getManagementEndpointValidationErrorCode(MULTICAST_IPV6) == ORG_ZSTACK_COMPUTE_HOST_10129 } } diff --git a/utils/src/main/java/org/zstack/utils/clouderrorcode/CloudOperationsErrorCode.java b/utils/src/main/java/org/zstack/utils/clouderrorcode/CloudOperationsErrorCode.java index dccd67723ac..3a272354846 100644 --- a/utils/src/main/java/org/zstack/utils/clouderrorcode/CloudOperationsErrorCode.java +++ b/utils/src/main/java/org/zstack/utils/clouderrorcode/CloudOperationsErrorCode.java @@ -11171,6 +11171,8 @@ public class CloudOperationsErrorCode { public static final String ORG_ZSTACK_COMPUTE_HOST_10128 = "ORG_ZSTACK_COMPUTE_HOST_10128"; + public static final String ORG_ZSTACK_COMPUTE_HOST_10129 = "ORG_ZSTACK_COMPUTE_HOST_10129"; + public static final String ORG_ZSTACK_MONITORING_TRIGGER_EXPRESSION_10000 = "ORG_ZSTACK_MONITORING_TRIGGER_EXPRESSION_10000"; public static final String ORG_ZSTACK_MONITORING_TRIGGER_EXPRESSION_10001 = "ORG_ZSTACK_MONITORING_TRIGGER_EXPRESSION_10001"; diff --git a/utils/src/main/java/org/zstack/utils/network/IPv6NetworkUtils.java b/utils/src/main/java/org/zstack/utils/network/IPv6NetworkUtils.java index 38d57c890b2..5acc04ab0cb 100644 --- a/utils/src/main/java/org/zstack/utils/network/IPv6NetworkUtils.java +++ b/utils/src/main/java/org/zstack/utils/network/IPv6NetworkUtils.java @@ -546,7 +546,7 @@ public static boolean isValidManagementEndpoint(String endpoint) { return isValidManagementIpv6Address(endpoint); } - private static boolean isValidManagementIpv6Address(String endpoint) { + public static boolean isValidManagementIpv6Address(String endpoint) { if (!isIpv6Address(endpoint)) { return false; } From 801f2f03ccf3f5d3713ed638618d87e1d0baee46 Mon Sep 17 00:00:00 2001 From: "shixin.ruan" Date: Fri, 22 May 2026 09:53:11 +0900 Subject: [PATCH 16/53] [mgt-ipv6]: harden extra ip cidr match Make KVM extra IP CIDR selection fail soft for blank or malformed agent-reported addresses, preserving management IP fallback semantics. Verification: - mvn -f plugin/kvm/pom.xml -DskipTests install - mvnTest -Dtest=ManagementNetworkIpv6Case Change-Id: Ic7e3b9d2fb3073d68c0f37c32b46cdeb25edcf2b Related: ZSTAC-79206 --- plugin/kvm/src/main/java/org/zstack/kvm/KVMHost.java | 12 ++++++++++-- .../core/ManagementNetworkIpv6Case.groovy | 1 + 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/plugin/kvm/src/main/java/org/zstack/kvm/KVMHost.java b/plugin/kvm/src/main/java/org/zstack/kvm/KVMHost.java index aeb796970ae..eae09b34930 100755 --- a/plugin/kvm/src/main/java/org/zstack/kvm/KVMHost.java +++ b/plugin/kvm/src/main/java/org/zstack/kvm/KVMHost.java @@ -2217,8 +2217,16 @@ public static String selectIpInCidr(String ips, String cidr) { final String[] ipList = ips.split(EXTRA_IP_SEPARATOR); for (String ip: ipList) { String trimmedIp = ip.trim(); - if (NetworkUtils.isIpInCidr(trimmedIp, cidr)) { - return trimmedIp; + if (StringUtils.isBlank(trimmedIp)) { + continue; + } + + try { + if (NetworkUtils.isIpInCidr(trimmedIp, cidr)) { + return trimmedIp; + } + } catch (RuntimeException e) { + logger.warn(String.format("skip invalid host extra IP[%s] when matching CIDR[%s]: %s", trimmedIp, cidr, e.getMessage())); } } diff --git a/test/src/test/groovy/org/zstack/test/integration/core/ManagementNetworkIpv6Case.groovy b/test/src/test/groovy/org/zstack/test/integration/core/ManagementNetworkIpv6Case.groovy index 822c5a6ed4d..7e70bde6e64 100644 --- a/test/src/test/groovy/org/zstack/test/integration/core/ManagementNetworkIpv6Case.groovy +++ b/test/src/test/groovy/org/zstack/test/integration/core/ManagementNetworkIpv6Case.groovy @@ -243,6 +243,7 @@ class ManagementNetworkIpv6Case { assert KVMHost.selectIpInCidr(HOST_EXTRA_IPS, "10.0.0.0/24") == "10.0.0.10" assert KVMHost.selectIpInCidr(HOST_EXTRA_IPS, "2001:db8::/64") == IPV6_2 assert KVMHost.selectIpInCidr(HOST_EXTRA_IPS, "172.16.0.0/16") == null + assert KVMHost.selectIpInCidr(" ,not-an-ip,${IPV6_2}", "2001:db8::/64") == IPV6_2 } void testApplianceVmBootstrapParam() { From 88520755ad3140d05bfd2d23a0f3161c66b11530 Mon Sep 17 00:00:00 2001 From: "shixin.ruan" Date: Fri, 22 May 2026 11:09:01 +0900 Subject: [PATCH 17/53] [mgt-ipv6]: skip reserved mn addresses Codex review found F-002 requires management server IP selection to ignore loopback and link-local addresses. Add the missing guard and focused coverage. Change-Id: Id638e90188b15d9e185495fdd0882f6cfa08a493 Related: ZSTAC-79206 --- core/src/main/java/org/zstack/core/Platform.java | 6 +++++- .../core/ManagementNetworkIpv6Case.groovy | 13 +++++++++++++ 2 files changed, 18 insertions(+), 1 deletion(-) diff --git a/core/src/main/java/org/zstack/core/Platform.java b/core/src/main/java/org/zstack/core/Platform.java index d002313271a..0c5af5202ec 100755 --- a/core/src/main/java/org/zstack/core/Platform.java +++ b/core/src/main/java/org/zstack/core/Platform.java @@ -1027,9 +1027,13 @@ public static String selectManagementServerIp(Collection addresses, for (InetAddress address : addresses) { String hostAddress = normalizeManagementIp(address.getHostAddress()); + if (address.isLoopbackAddress() || address.isLinkLocalAddress()) { + continue; + } + if (address instanceof Inet4Address) { ipv4 = hostAddress; - } else if (!IPv6NetworkUtils.isLinkLocalAddress(hostAddress)) { + } else { ipv6 = hostAddress; } } diff --git a/test/src/test/groovy/org/zstack/test/integration/core/ManagementNetworkIpv6Case.groovy b/test/src/test/groovy/org/zstack/test/integration/core/ManagementNetworkIpv6Case.groovy index 7e70bde6e64..f729d133020 100644 --- a/test/src/test/groovy/org/zstack/test/integration/core/ManagementNetworkIpv6Case.groovy +++ b/test/src/test/groovy/org/zstack/test/integration/core/ManagementNetworkIpv6Case.groovy @@ -54,6 +54,7 @@ class ManagementNetworkIpv6Case { testPreferIpv6DefaultFalse() testPreferIpv6SystemProperty() testSelectManagementServerIpDualStackPolicy() + testSelectManagementServerIpSkipsLoopbackAndLinkLocal() testSelectApplianceVmManagementNodeIpByCidr() testBuildUrlIpv4() testBuildUrlIpv6() @@ -106,6 +107,18 @@ class ManagementNetworkIpv6Case { assert Platform.selectManagementServerIp([ipv4], true) == IPV4 } + void testSelectManagementServerIpSkipsLoopbackAndLinkLocal() { + def ipv4 = InetAddress.getByName(IPV4) + def ipv6 = InetAddress.getByName(IPV6) + def loopbackIpv4 = InetAddress.getByName("127.0.0.1") + def loopbackIpv6 = InetAddress.getByName(LOOPBACK_IPV6) + def linkLocalIpv6 = InetAddress.getByName(LINK_LOCAL_IPV6) + + assert Platform.selectManagementServerIp([loopbackIpv4, ipv4], false) == IPV4 + assert Platform.selectManagementServerIp([loopbackIpv6, linkLocalIpv6, ipv6], true) == IPV6 + assert Platform.selectManagementServerIp([loopbackIpv4, loopbackIpv6, linkLocalIpv6], true) == null + } + void testSelectApplianceVmManagementNodeIpByCidr() { assert ApplianceVmFacadeImpl.selectManagementNodeIpForBootstrap( [IPV4, IPV6], From 67617e25fa0f28eabcb01aafdf2c56cf18c431dd Mon Sep 17 00:00:00 2001 From: "shixin.ruan" Date: Fri, 22 May 2026 11:14:17 +0900 Subject: [PATCH 18/53] [mgt-ipv6]: support kvm tcp ipv6 Codex review found the KVM host TCP connection checker still bound to an IPv4-only wildcard and parsed remote addresses by splitting on colon. This breaks IPv6 host management connections. Use wildcard bind and parse InetSocketAddress directly so IPv6 addresses are preserved and normalized. Change-Id: Ib7d448af7d80d341d2115320ea67a92e2287f1dd Related: ZSTAC-79206 --- .../java/org/zstack/kvm/KVMHostFactory.java | 33 +++++++++++++++++-- .../zstack/kvm/KVMHostFactoryIpv6Case.groovy | 21 ++++++++++++ 2 files changed, 52 insertions(+), 2 deletions(-) create mode 100644 test/src/test/groovy/org/zstack/kvm/KVMHostFactoryIpv6Case.groovy diff --git a/plugin/kvm/src/main/java/org/zstack/kvm/KVMHostFactory.java b/plugin/kvm/src/main/java/org/zstack/kvm/KVMHostFactory.java index cb58a569636..837a8fce986 100755 --- a/plugin/kvm/src/main/java/org/zstack/kvm/KVMHostFactory.java +++ b/plugin/kvm/src/main/java/org/zstack/kvm/KVMHostFactory.java @@ -87,10 +87,12 @@ import org.zstack.utils.function.ValidateFunction; import org.zstack.utils.gson.JSONObjectUtil; import org.zstack.utils.logging.CLogger; +import org.zstack.utils.network.IPv6NetworkUtils; import javax.persistence.Tuple; import java.io.File; import java.io.IOException; +import java.net.InetAddress; import java.net.InetSocketAddress; import java.net.SocketAddress; import java.net.StandardSocketOptions; @@ -958,7 +960,7 @@ private boolean isTimeout(Long timeInMap, long currentTime) { @AsyncThread private void startTcpServer() throws IOException { try (Selector selector = Selector.open(); ServerSocketChannel serverSocket = ServerSocketChannel.open()) { - serverSocket.bind(new InetSocketAddress("0.0.0.0", KVMGlobalProperty.TCP_SERVER_PORT)); + serverSocket.bind(makeTcpServerBindAddress(KVMGlobalProperty.TCP_SERVER_PORT)); serverSocket.configureBlocking(false); serverSocket.register(selector, SelectionKey.OP_ACCEPT); ByteBuffer buffer = ByteBuffer.allocate(256); @@ -1005,7 +1007,11 @@ private void checkHostConnection(SocketChannel client) throws IOException { client.close(); socketTimeoutMap.remove(client, timeHelper.getCurrentTimeMillis()); - String managementIp = remoteAddress.toString().split("/")[1].split(":")[0]; + String managementIp = extractRemoteManagementIp(remoteAddress); + if (managementIp == null) { + return; + } + String hostUuid = Q.New(HostVO.class) .select(HostVO_.uuid) .eq(HostVO_.managementIp, managementIp) @@ -1033,6 +1039,29 @@ private void register(Selector selector, ServerSocketChannel serverSocket) socketTimeoutMap.put(client, timeHelper.getCurrentTimeMillis()); } + static InetSocketAddress makeTcpServerBindAddress(int port) { + return new InetSocketAddress(port); + } + + static String extractRemoteManagementIp(SocketAddress remoteAddress) { + if (!(remoteAddress instanceof InetSocketAddress)) { + return null; + } + + InetAddress address = ((InetSocketAddress) remoteAddress).getAddress(); + if (address == null) { + return null; + } + + String hostAddress = address.getHostAddress(); + int scopeIndex = hostAddress.indexOf('%'); + if (scopeIndex >= 0) { + hostAddress = hostAddress.substring(0, scopeIndex); + } + + return IPv6NetworkUtils.isIpv6Address(hostAddress) ? IPv6NetworkUtils.normalizeIpv6(hostAddress) : hostAddress; + } + private Map getHostsWithDiffModel(String clusterUuid) { List hostUuidsInCluster = Q.New(HostVO.class) .select(HostVO_.uuid) diff --git a/test/src/test/groovy/org/zstack/kvm/KVMHostFactoryIpv6Case.groovy b/test/src/test/groovy/org/zstack/kvm/KVMHostFactoryIpv6Case.groovy new file mode 100644 index 00000000000..41f04fd7dde --- /dev/null +++ b/test/src/test/groovy/org/zstack/kvm/KVMHostFactoryIpv6Case.groovy @@ -0,0 +1,21 @@ +package org.zstack.kvm + +import org.junit.Test + +import java.net.InetSocketAddress + +class KVMHostFactoryIpv6Case { + @Test + void testExtractRemoteManagementIpSupportsIpv4AndIpv6() { + assert KVMHostFactory.extractRemoteManagementIp(new InetSocketAddress("192.168.10.10", 7123)) == "192.168.10.10" + assert KVMHostFactory.extractRemoteManagementIp(new InetSocketAddress("2001:db8::10", 7123)) == "2001:db8::10" + } + + @Test + void testTcpServerBindAddressUsesWildcardAddress() { + InetSocketAddress bindAddress = KVMHostFactory.makeTcpServerBindAddress(7123) + + assert bindAddress.port == 7123 + assert bindAddress.address.anyLocalAddress + } +} From e251b2ed2f8e8c3e8ef80cfd8021db0b2a126cf3 Mon Sep 17 00:00:00 2001 From: "shixin.ruan" Date: Fri, 22 May 2026 11:48:50 +0900 Subject: [PATCH 19/53] [console]: support ipv6 console urls Format console, KVM VNC, and legacy HTTP helper URLs with bracketed IPv6 hosts. Resolves: ZSTAC-79206 Change-Id: I202dfd4feca366e2badf88f16d1423dea304aa41 --- .../zstack/console/ConsoleManagerImpl.java | 19 +++++++++++++------ .../kvm/KVMConsoleHypervisorBackend.java | 7 ++++++- .../core/ManagementNetworkIpv6Case.groovy | 18 ++++++++++++++++++ .../java/org/zstack/utils/URLBuilder.java | 7 +++++-- 4 files changed, 42 insertions(+), 9 deletions(-) diff --git a/console/src/main/java/org/zstack/console/ConsoleManagerImpl.java b/console/src/main/java/org/zstack/console/ConsoleManagerImpl.java index 67057da171d..b528d0fdcc5 100755 --- a/console/src/main/java/org/zstack/console/ConsoleManagerImpl.java +++ b/console/src/main/java/org/zstack/console/ConsoleManagerImpl.java @@ -31,6 +31,7 @@ import org.zstack.header.vm.*; import org.zstack.utils.Utils; import org.zstack.utils.logging.CLogger; +import org.zstack.utils.network.IPv6NetworkUtils; import javax.persistence.Query; import java.util.HashMap; @@ -44,6 +45,8 @@ */ public class ConsoleManagerImpl extends AbstractService implements ConsoleManager, VmInstanceMigrateExtensionPoint, ManagementNodeChangeListener, VmReleaseResourceExtensionPoint, SessionLogoutExtensionPoint, PostVmInstantiateResourceExtensionPoint, KvmReportVmShutdownFromGuestEventExtensionPoint { + static final String ANY_IPV4_ADDRESS = "0.0.0.0"; + static final String UNIT_TEST_CONSOLE_PROXY_HOST = "127.0.0.1"; private static CLogger logger = Utils.getLogger(ConsoleManagerImpl.class); @Autowired @@ -125,12 +128,10 @@ public void fail(ErrorCode errorCode) { } private void overriddenConsoleProxyIP(ConsoleInventory consoleInventory) { - if (!"0.0.0.0".equals(CoreGlobalProperty.CONSOLE_PROXY_OVERRIDDEN_IP) && - !"".equals(CoreGlobalProperty.CONSOLE_PROXY_OVERRIDDEN_IP)) { - consoleInventory.setHostname(CoreGlobalProperty.CONSOLE_PROXY_OVERRIDDEN_IP); - } else { - consoleInventory.setHostname(CoreGlobalProperty.UNIT_TEST_ON ? "127.0.0.1" : Platform.getManagementServerIp()); - } + consoleInventory.setHostname(selectConsoleProxyHostname( + CoreGlobalProperty.CONSOLE_PROXY_OVERRIDDEN_IP, + CoreGlobalProperty.UNIT_TEST_ON, + Platform.getManagementServerIp())); } @Override @@ -140,6 +141,12 @@ public String getName() { }); } + static String selectConsoleProxyHostname(String overriddenIp, boolean unitTestOn, String managementServerIp) { + String hostname = overriddenIp != null && !overriddenIp.isEmpty() && !ANY_IPV4_ADDRESS.equals(overriddenIp) ? + overriddenIp : (unitTestOn ? UNIT_TEST_CONSOLE_PROXY_HOST : managementServerIp); + return IPv6NetworkUtils.formatHostForUrl(hostname); + } + @Override public String getId() { return bus.makeLocalServiceId(ConsoleConstants.SERVICE_ID); diff --git a/plugin/kvm/src/main/java/org/zstack/kvm/KVMConsoleHypervisorBackend.java b/plugin/kvm/src/main/java/org/zstack/kvm/KVMConsoleHypervisorBackend.java index 3e8ab0c26a3..80b3e3d0cef 100755 --- a/plugin/kvm/src/main/java/org/zstack/kvm/KVMConsoleHypervisorBackend.java +++ b/plugin/kvm/src/main/java/org/zstack/kvm/KVMConsoleHypervisorBackend.java @@ -16,6 +16,7 @@ import org.zstack.header.message.MessageReply; import org.zstack.header.vm.VmInstanceInventory; import org.zstack.kvm.KVMAgentCommands.GetVncPortResponse; +import org.zstack.utils.network.IPv6NetworkUtils; import java.net.URI; import java.net.URISyntaxException; @@ -78,7 +79,7 @@ public void run(MessageReply reply) { String mgmtIp = q.findValue(); try { // see https://tools.ietf.org/html/rfc7869#section-2.1 - URI uri = new URI(String.format("vnc://%s:%s/", mgmtIp, rsp.getPort())); + URI uri = buildConsoleUri(mgmtIp, rsp.getPort()); ConsoleUrl consoleUrl = new ConsoleUrl(); consoleUrl.setUri(uri); consoleUrl.setVersion(dbf.getDbVersion()); @@ -89,4 +90,8 @@ public void run(MessageReply reply) { } }); } + + public static URI buildConsoleUri(String host, int port) throws URISyntaxException { + return new URI(String.format("vnc://%s:%s/", IPv6NetworkUtils.formatHostForUrl(host), port)); + } } diff --git a/test/src/test/groovy/org/zstack/test/integration/core/ManagementNetworkIpv6Case.groovy b/test/src/test/groovy/org/zstack/test/integration/core/ManagementNetworkIpv6Case.groovy index f729d133020..c7dd8dd719f 100644 --- a/test/src/test/groovy/org/zstack/test/integration/core/ManagementNetworkIpv6Case.groovy +++ b/test/src/test/groovy/org/zstack/test/integration/core/ManagementNetworkIpv6Case.groovy @@ -6,10 +6,12 @@ import org.zstack.core.NetworkGlobalConfig import org.zstack.core.Platform import org.zstack.core.rest.RESTFacadeImpl import org.zstack.header.rest.RESTConstant +import org.zstack.kvm.KVMConsoleHypervisorBackend import org.zstack.kvm.KVMHost import org.zstack.network.l2.vxlan.vxlanNetworkPool.VxlanPoolApiInterceptor import org.zstack.storage.ceph.MonUri import org.zstack.storage.primary.nfs.NfsApiParamChecker +import org.zstack.utils.URLBuilder import org.zstack.utils.network.IPv6Constants import org.zstack.utils.network.IPv6NetworkUtils import org.zstack.utils.network.NetworkUtils @@ -58,6 +60,8 @@ class ManagementNetworkIpv6Case { testSelectApplianceVmManagementNodeIpByCidr() testBuildUrlIpv4() testBuildUrlIpv6() + testLegacyUrlBuilderIpv6() + testConsoleVncUriIpv6() testRestFacadeIpv6Urls() testBuildHostPortIpv6() testBracketIpv6Idempotent() @@ -142,6 +146,20 @@ class ManagementNetworkIpv6Case { assert IPv6NetworkUtils.buildHttpUrl(IPV6, REST_PORT) == "http://[2001:db8::1]:8080" } + void testLegacyUrlBuilderIpv6() { + assert URLBuilder.buildHttpUrl(IPV6, REST_PORT, "/console/establish") == + "http://[2001:db8::1]:8080/console/establish" + assert URLBuilder.buildSslHttpUrl(IPV6, REST_PORT, "/console/establish") == + "https://[2001:db8::1]:8080/console/establish" + } + + void testConsoleVncUriIpv6() { + URI uri = KVMConsoleHypervisorBackend.buildConsoleUri(IPV6, REST_PORT) + assert uri.toString() == "vnc://[2001:db8::1]:8080/" + assert uri.host == "[${IPV6}]" + assert uri.port == REST_PORT + } + void testRestFacadeIpv6Urls() { assert RESTFacadeImpl.buildBaseUrl(IPV6, REST_PORT, null) == "http://[2001:db8::1]:8080" assert RESTFacadeImpl.buildBaseUrl(IPV6, REST_PORT, "zstack") == "http://[2001:db8::1]:8080/zstack" diff --git a/utils/src/main/java/org/zstack/utils/URLBuilder.java b/utils/src/main/java/org/zstack/utils/URLBuilder.java index cc2ed0ddb4c..047323db74c 100755 --- a/utils/src/main/java/org/zstack/utils/URLBuilder.java +++ b/utils/src/main/java/org/zstack/utils/URLBuilder.java @@ -2,11 +2,14 @@ import org.springframework.web.util.UriComponents; import org.springframework.web.util.UriComponentsBuilder; +import org.zstack.utils.network.IPv6NetworkUtils; public class URLBuilder { + private static final String BASE_URL_FORMAT = "%s://%s:%s"; + public static String buildUrl(String scheme, String host, int port, String...paths) { - UriComponentsBuilder builder = UriComponentsBuilder.newInstance(); - builder.scheme(scheme).host(host).port(port); + UriComponentsBuilder builder = UriComponentsBuilder.fromUriString( + String.format(BASE_URL_FORMAT, scheme, IPv6NetworkUtils.formatHostForUrl(host), port)); for (String p : paths) { builder.path(p); } From dad751c8c23447f74632e5eb8af1347de63e3d43 Mon Sep 17 00:00:00 2001 From: "shixin.ruan" Date: Fri, 22 May 2026 12:26:43 +0900 Subject: [PATCH 20/53] [mgt-ipv6]: preserve ipv6 ipmi address ZSTAC-79206 Change-Id: Ie9c04ecf5b6778c1acdc4900e8726fcd8ae3d81e Related: ZSTAC-79206 --- .../java/org/zstack/kvm/KvmHostIpmiPowerExecutor.java | 8 +++++--- .../integration/core/ManagementNetworkIpv6Case.groovy | 9 +++++++++ 2 files changed, 14 insertions(+), 3 deletions(-) diff --git a/plugin/kvm/src/main/java/org/zstack/kvm/KvmHostIpmiPowerExecutor.java b/plugin/kvm/src/main/java/org/zstack/kvm/KvmHostIpmiPowerExecutor.java index 025d3913729..ab804fb8235 100644 --- a/plugin/kvm/src/main/java/org/zstack/kvm/KvmHostIpmiPowerExecutor.java +++ b/plugin/kvm/src/main/java/org/zstack/kvm/KvmHostIpmiPowerExecutor.java @@ -35,9 +35,7 @@ protected void run(Map tokens, Object data) { String hostUuid = d.getInventory().getUuid(); String currentIpmiAddress = HostSystemTags.IPMI_ADDRESS.getTokenByResourceUuid(hostUuid, HostSystemTags.IPMI_ADDRESS_TOKEN); HostPowerStatus status = HostPowerStatus.POWER_ON; - if (!NetworkUtils.isIpv4Address(currentIpmiAddress)) { - currentIpmiAddress = null; - } + currentIpmiAddress = normalizeIpmiAddress(currentIpmiAddress); HostVO host = dbf.findByUuid(hostUuid, HostVO.class); HostIpmiVO ipmi = host.getIpmi(); @@ -71,4 +69,8 @@ public boolean start() { public boolean stop() { return true; } + + public static String normalizeIpmiAddress(String ipmiAddress) { + return NetworkUtils.isValidIPAddress(ipmiAddress) ? ipmiAddress : null; + } } diff --git a/test/src/test/groovy/org/zstack/test/integration/core/ManagementNetworkIpv6Case.groovy b/test/src/test/groovy/org/zstack/test/integration/core/ManagementNetworkIpv6Case.groovy index c7dd8dd719f..bfe6d25b0b8 100644 --- a/test/src/test/groovy/org/zstack/test/integration/core/ManagementNetworkIpv6Case.groovy +++ b/test/src/test/groovy/org/zstack/test/integration/core/ManagementNetworkIpv6Case.groovy @@ -8,6 +8,7 @@ import org.zstack.core.rest.RESTFacadeImpl import org.zstack.header.rest.RESTConstant import org.zstack.kvm.KVMConsoleHypervisorBackend import org.zstack.kvm.KVMHost +import org.zstack.kvm.KvmHostIpmiPowerExecutor import org.zstack.network.l2.vxlan.vxlanNetworkPool.VxlanPoolApiInterceptor import org.zstack.storage.ceph.MonUri import org.zstack.storage.primary.nfs.NfsApiParamChecker @@ -78,6 +79,7 @@ class ManagementNetworkIpv6Case { testCephIpv6MonUrlParsing() testVxlanVtepIpv6Validation() testKvmExtraIpCidrSelection() + testKvmIpmiAddressKeepsIpv6() testApplianceVmBootstrapParam() } @@ -277,6 +279,13 @@ class ManagementNetworkIpv6Case { assert KVMHost.selectIpInCidr(" ,not-an-ip,${IPV6_2}", "2001:db8::/64") == IPV6_2 } + void testKvmIpmiAddressKeepsIpv6() { + assert KvmHostIpmiPowerExecutor.normalizeIpmiAddress(IPV4) == IPV4 + assert KvmHostIpmiPowerExecutor.normalizeIpmiAddress(IPV6) == IPV6 + assert KvmHostIpmiPowerExecutor.normalizeIpmiAddress(INVALID_IP) == null + assert KvmHostIpmiPowerExecutor.normalizeIpmiAddress(null) == null + } + void testApplianceVmBootstrapParam() { assert ApplianceVmConstant.BootstrapParams.managementNodeIp6Cidr.toString() == "managementNodeIp6Cidr" } From c941174d98867bd508d3a6803f5f3bb21f52fc44 Mon Sep 17 00:00:00 2001 From: "shixin.ruan" Date: Fri, 22 May 2026 15:09:50 +0900 Subject: [PATCH 21/53] [mgt-ipv6]: bind console proxy on ipv6 ZSTAC-79206 Change-Id: I165d7c0183291574588e3ede9aae3c71ce0ffa2e Related: ZSTAC-79206 --- .../main/java/org/zstack/console/ConsoleProxyBase.java | 9 ++++++++- .../integration/core/ManagementNetworkIpv6Case.groovy | 8 ++++++++ 2 files changed, 16 insertions(+), 1 deletion(-) diff --git a/console/src/main/java/org/zstack/console/ConsoleProxyBase.java b/console/src/main/java/org/zstack/console/ConsoleProxyBase.java index c61a211655e..92f3a77b036 100755 --- a/console/src/main/java/org/zstack/console/ConsoleProxyBase.java +++ b/console/src/main/java/org/zstack/console/ConsoleProxyBase.java @@ -19,6 +19,7 @@ import org.zstack.utils.URLBuilder; import org.zstack.utils.Utils; import org.zstack.utils.logging.CLogger; +import org.zstack.utils.network.IPv6NetworkUtils; import java.net.URI; import java.sql.Timestamp; @@ -35,6 +36,8 @@ @Configurable(preConstruction = true, autowire = Autowire.BY_TYPE) public class ConsoleProxyBase implements ConsoleProxy { private static final CLogger logger = Utils.getLogger(ConsoleProxyBase.class); + static final String ANY_IPV4_ADDRESS = "0.0.0.0"; + static final String ANY_IPV6_ADDRESS = "::"; private ConsoleProxyInventory self; @Autowired @@ -79,7 +82,7 @@ private void doEstablishConsoleProxyConnection(ConsoleUrl consoleUrl, final Retu cmd.setTargetSchema(targetSchema); cmd.setTargetHostname(targetHostname); cmd.setTargetPort(targetPort); - cmd.setProxyHostname("0.0.0.0"); + cmd.setProxyHostname(selectProxyListenHostname(self.getAgentIp())); if (ConsoleConstants.HTTP_SCHEMA.equals(targetSchema)) { cmd.setProxyPort(CoreGlobalProperty.HTTP_CONSOLE_PROXY_PORT); } else { @@ -125,6 +128,10 @@ public Class getReturnClass() { }); } + public static String selectProxyListenHostname(String agentIp) { + return IPv6NetworkUtils.isIpv6Address(agentIp) ? ANY_IPV6_ADDRESS : ANY_IPV4_ADDRESS; + } + void doEstablishDirectConsoleConnection(ConsoleUrl consoleUrl, final ReturnValueCompletion completion) { URI uri = consoleUrl.getUri(); diff --git a/test/src/test/groovy/org/zstack/test/integration/core/ManagementNetworkIpv6Case.groovy b/test/src/test/groovy/org/zstack/test/integration/core/ManagementNetworkIpv6Case.groovy index bfe6d25b0b8..e870777e986 100644 --- a/test/src/test/groovy/org/zstack/test/integration/core/ManagementNetworkIpv6Case.groovy +++ b/test/src/test/groovy/org/zstack/test/integration/core/ManagementNetworkIpv6Case.groovy @@ -5,6 +5,7 @@ import org.zstack.appliancevm.ApplianceVmFacadeImpl import org.zstack.core.NetworkGlobalConfig import org.zstack.core.Platform import org.zstack.core.rest.RESTFacadeImpl +import org.zstack.console.ConsoleProxyBase import org.zstack.header.rest.RESTConstant import org.zstack.kvm.KVMConsoleHypervisorBackend import org.zstack.kvm.KVMHost @@ -63,6 +64,7 @@ class ManagementNetworkIpv6Case { testBuildUrlIpv6() testLegacyUrlBuilderIpv6() testConsoleVncUriIpv6() + testConsoleProxyListenHostByAgentIpVersion() testRestFacadeIpv6Urls() testBuildHostPortIpv6() testBracketIpv6Idempotent() @@ -162,6 +164,12 @@ class ManagementNetworkIpv6Case { assert uri.port == REST_PORT } + void testConsoleProxyListenHostByAgentIpVersion() { + assert ConsoleProxyBase.selectProxyListenHostname(IPV6) == "::" + assert ConsoleProxyBase.selectProxyListenHostname(IPV4) == "0.0.0.0" + assert ConsoleProxyBase.selectProxyListenHostname("mn.example.com") == "0.0.0.0" + } + void testRestFacadeIpv6Urls() { assert RESTFacadeImpl.buildBaseUrl(IPV6, REST_PORT, null) == "http://[2001:db8::1]:8080" assert RESTFacadeImpl.buildBaseUrl(IPV6, REST_PORT, "zstack") == "http://[2001:db8::1]:8080/zstack" From b50f03433e014e5053c27ffa265b2f7ef30c85fb Mon Sep 17 00:00:00 2001 From: "shixin.ruan" Date: Fri, 22 May 2026 15:14:53 +0900 Subject: [PATCH 22/53] [mgt-ipv6]: bracket core management urls ZSTAC-79206 Change-Id: I92d84c8d9521ebfd87b749145f80211db4ac88f5 Related: ZSTAC-79206 --- .../org/zstack/core/agent/AgentManagerImpl.java | 8 +++++++- .../java/org/zstack/core/ansible/AnsibleRunner.java | 11 +++++++++-- .../org/zstack/core/cloudbus/CloudBusImpl3.java | 13 ++++++++++--- .../core/ManagementNetworkIpv6Case.groovy | 10 ++++++++++ 4 files changed, 36 insertions(+), 6 deletions(-) diff --git a/core/src/main/java/org/zstack/core/agent/AgentManagerImpl.java b/core/src/main/java/org/zstack/core/agent/AgentManagerImpl.java index 0e0482f2442..27966a5c73c 100755 --- a/core/src/main/java/org/zstack/core/agent/AgentManagerImpl.java +++ b/core/src/main/java/org/zstack/core/agent/AgentManagerImpl.java @@ -26,6 +26,7 @@ import org.zstack.header.rest.RESTFacade; import org.zstack.utils.Utils; import org.zstack.utils.logging.CLogger; +import org.zstack.utils.network.IPv6NetworkUtils; import org.zstack.utils.network.NetworkUtils; import org.zstack.utils.path.PathUtil; import org.zstack.utils.ssh.Ssh; @@ -45,6 +46,11 @@ */ public class AgentManagerImpl extends AbstractService implements AgentManager { private static final CLogger logger = Utils.getLogger(AgentManagerImpl.class); + private static final String HTTP_URL_FORMAT = "http://%s:%s%s"; + + public static String buildAgentUrl(String ip, int port, String path) { + return String.format(HTTP_URL_FORMAT, IPv6NetworkUtils.formatHostForUrl(ip), port, path); + } @Autowired private CloudBus bus; @@ -113,7 +119,7 @@ private void connect(final DeployAgentMsg msg, final Completion completion) { chain.setName(String.format("continue-connect-agent-server-%s:%s", msg.getIp(), msg.getAgentPort())); chain.then(new ShareFlow() { private String url(String path) { - return String.format("http://%s:%s%s", msg.getIp(), msg.getAgentPort(), path); + return buildAgentUrl(msg.getIp(), msg.getAgentPort(), path); } @Override diff --git a/core/src/main/java/org/zstack/core/ansible/AnsibleRunner.java b/core/src/main/java/org/zstack/core/ansible/AnsibleRunner.java index b05e66ef2ff..bd7e06ec714 100755 --- a/core/src/main/java/org/zstack/core/ansible/AnsibleRunner.java +++ b/core/src/main/java/org/zstack/core/ansible/AnsibleRunner.java @@ -16,6 +16,7 @@ import org.zstack.utils.ShellUtils; import org.zstack.utils.Utils; import org.zstack.utils.logging.CLogger; +import org.zstack.utils.network.IPv6NetworkUtils; import org.zstack.utils.network.NetworkUtils; import org.zstack.utils.path.PathUtil; import org.zstack.utils.ssh.Ssh; @@ -40,6 +41,8 @@ @Configurable(preConstruction = true, autowire = Autowire.BY_TYPE) public class AnsibleRunner { private static final CLogger logger = Utils.getLogger(AnsibleRunner.class); + static final String HTTP_URL_FORMAT = "http://%s:%d%s"; + static final String PYPI_SIMPLE_PATH = "/zstack/static/pypi/simple"; @Autowired private AnsibleFacade asf; @@ -390,9 +393,9 @@ public void run(ReturnValueCompletion completion) { deployArguments = new AnsibleBasicArguments(); } - deployArguments.setPipUrl(String.format("http://%s:%d/zstack/static/pypi/simple", restf.getHostName(), port)); + deployArguments.setPipUrl(buildPipUrl(restf.getHostName(), port)); deployArguments.setTrustedHost(restf.getHostName()); - deployArguments.setYumServer(String.format("%s:%d", restf.getHostName(), port)); + deployArguments.setYumServer(IPv6NetworkUtils.formatHostPort(restf.getHostName(), port)); deployArguments.setRemoteUser(username); if (password != null && !password.isEmpty()) { deployArguments.setRemotePass(password); @@ -415,6 +418,10 @@ public void run(ReturnValueCompletion completion) { } + public static String buildPipUrl(String hostname, int port) { + return String.format(HTTP_URL_FORMAT, IPv6NetworkUtils.formatHostForUrl(hostname), port, PYPI_SIMPLE_PATH); + } + public void restartAgent(String agentName, Completion completion) { String script = String.format("sudo bash /etc/init.d/%s restart\n", agentName); diff --git a/core/src/main/java/org/zstack/core/cloudbus/CloudBusImpl3.java b/core/src/main/java/org/zstack/core/cloudbus/CloudBusImpl3.java index 93e8555b875..166e7323918 100755 --- a/core/src/main/java/org/zstack/core/cloudbus/CloudBusImpl3.java +++ b/core/src/main/java/org/zstack/core/cloudbus/CloudBusImpl3.java @@ -66,6 +66,7 @@ import org.zstack.utils.Utils; import org.zstack.utils.gson.JSONObjectUtil; import org.zstack.utils.logging.CLogger; +import org.zstack.utils.network.IPv6NetworkUtils; import javax.servlet.http.HttpServletResponse; import java.io.IOException; @@ -140,8 +141,16 @@ public class CloudBusImpl3 implements CloudBus, CloudBusIN { private final static TimeoutRestTemplate http = RESTFacade.createRestTemplate(CoreGlobalProperty.REST_FACADE_READ_TIMEOUT, CoreGlobalProperty.REST_FACADE_CONNECT_TIMEOUT); public static final String HTTP_BASE_URL = "/cloudbus"; + private static final String HTTP_URL_FORMAT = "http://%s:%s%s"; + private static final String HTTP_CONTEXT_URL_FORMAT = "http://%s:%s/%s/%s"; public static final FutureCompletion SEND_CONFIRMED = new FutureCompletion(null); + public static String buildCloudBusUrl(String ip, int port, String contextPath) { + String host = IPv6NetworkUtils.formatHostForUrl(ip); + return contextPath.isEmpty() ? String.format(HTTP_URL_FORMAT, host, port, HTTP_BASE_URL) : + String.format(HTTP_CONTEXT_URL_FORMAT, host, port, contextPath, HTTP_BASE_URL); + } + private TelemetryFacade telemetryFacade; private TelemetryFacade getTelemetryFacade() { @@ -673,9 +682,7 @@ private FutureCompletion httpSend() { } private void httpSend(String ip) { - String url = CloudBusGlobalProperty.HTTP_CONTEXT_PATH.isEmpty() ? String.format("http://%s:%s%s", - ip, CloudBusGlobalProperty.HTTP_PORT, HTTP_BASE_URL) : String.format("http://%s:%s/%s/%s", - ip, CloudBusGlobalProperty.HTTP_PORT, CloudBusGlobalProperty.HTTP_CONTEXT_PATH, HTTP_BASE_URL); + String url = CloudBusImpl3.buildCloudBusUrl(ip, CloudBusGlobalProperty.HTTP_PORT, CloudBusGlobalProperty.HTTP_CONTEXT_PATH); HttpHeaders headers = new HttpHeaders(); HttpEntity req = new HttpEntity<>(CloudBusGson.toJson(msg), headers); diff --git a/test/src/test/groovy/org/zstack/test/integration/core/ManagementNetworkIpv6Case.groovy b/test/src/test/groovy/org/zstack/test/integration/core/ManagementNetworkIpv6Case.groovy index e870777e986..19299d52d00 100644 --- a/test/src/test/groovy/org/zstack/test/integration/core/ManagementNetworkIpv6Case.groovy +++ b/test/src/test/groovy/org/zstack/test/integration/core/ManagementNetworkIpv6Case.groovy @@ -2,8 +2,11 @@ package org.zstack.test.integration.core import org.zstack.appliancevm.ApplianceVmConstant import org.zstack.appliancevm.ApplianceVmFacadeImpl +import org.zstack.core.ansible.AnsibleRunner import org.zstack.core.NetworkGlobalConfig import org.zstack.core.Platform +import org.zstack.core.agent.AgentManagerImpl +import org.zstack.core.cloudbus.CloudBusImpl3 import org.zstack.core.rest.RESTFacadeImpl import org.zstack.console.ConsoleProxyBase import org.zstack.header.rest.RESTConstant @@ -65,6 +68,7 @@ class ManagementNetworkIpv6Case { testLegacyUrlBuilderIpv6() testConsoleVncUriIpv6() testConsoleProxyListenHostByAgentIpVersion() + testCoreManagementUrlsIpv6() testRestFacadeIpv6Urls() testBuildHostPortIpv6() testBracketIpv6Idempotent() @@ -170,6 +174,12 @@ class ManagementNetworkIpv6Case { assert ConsoleProxyBase.selectProxyListenHostname("mn.example.com") == "0.0.0.0" } + void testCoreManagementUrlsIpv6() { + assert CloudBusImpl3.buildCloudBusUrl(IPV6, REST_PORT, "") == "http://[2001:db8::1]:8080/cloudbus" + assert AgentManagerImpl.buildAgentUrl(IPV6, REST_PORT, "/agent/echo") == "http://[2001:db8::1]:8080/agent/echo" + assert AnsibleRunner.buildPipUrl(IPV6, REST_PORT) == "http://[2001:db8::1]:8080/zstack/static/pypi/simple" + } + void testRestFacadeIpv6Urls() { assert RESTFacadeImpl.buildBaseUrl(IPV6, REST_PORT, null) == "http://[2001:db8::1]:8080" assert RESTFacadeImpl.buildBaseUrl(IPV6, REST_PORT, "zstack") == "http://[2001:db8::1]:8080/zstack" From d8a7106f75a5adf2a3ee94603881b48610993464 Mon Sep 17 00:00:00 2001 From: "shixin.ruan" Date: Fri, 22 May 2026 15:55:26 +0900 Subject: [PATCH 23/53] [mgt-ipv6]: bracket java ssh targets Resolves: ZSTAC-79206 Change-Id: I6b47c3a551747faa651e62061c3c11654ac4e183 --- .../core/ManagementNetworkIpv6Case.groovy | 8 ++++++++ .../src/main/java/org/zstack/utils/ssh/Ssh.java | 5 +++-- .../main/java/org/zstack/utils/ssh/SshShell.java | 16 +++++++++++----- 3 files changed, 22 insertions(+), 7 deletions(-) diff --git a/test/src/test/groovy/org/zstack/test/integration/core/ManagementNetworkIpv6Case.groovy b/test/src/test/groovy/org/zstack/test/integration/core/ManagementNetworkIpv6Case.groovy index 19299d52d00..8409423dbb5 100644 --- a/test/src/test/groovy/org/zstack/test/integration/core/ManagementNetworkIpv6Case.groovy +++ b/test/src/test/groovy/org/zstack/test/integration/core/ManagementNetworkIpv6Case.groovy @@ -17,6 +17,7 @@ import org.zstack.network.l2.vxlan.vxlanNetworkPool.VxlanPoolApiInterceptor import org.zstack.storage.ceph.MonUri import org.zstack.storage.primary.nfs.NfsApiParamChecker import org.zstack.utils.URLBuilder +import org.zstack.utils.ssh.SshShell import org.zstack.utils.network.IPv6Constants import org.zstack.utils.network.IPv6NetworkUtils import org.zstack.utils.network.NetworkUtils @@ -70,6 +71,7 @@ class ManagementNetworkIpv6Case { testConsoleProxyListenHostByAgentIpVersion() testCoreManagementUrlsIpv6() testRestFacadeIpv6Urls() + testSshTargetUsesBracketedIpv6Host() testBuildHostPortIpv6() testBracketIpv6Idempotent() testNormalizeIpv6() @@ -189,6 +191,12 @@ class ManagementNetworkIpv6Case { "http://[2001:db8::1]:8080/zstack${RESTConstant.COMMAND_CHANNEL_PATH}" } + void testSshTargetUsesBracketedIpv6Host() { + assert SshShell.formatSshTarget("root", IPV4) == "root@192.168.1.10" + assert SshShell.formatSshTarget("root", IPV6) == "root@[2001:db8::1]" + assert SshShell.formatSshTarget("root", "host-01.example.com") == "root@host-01.example.com" + } + void testBuildHostPortIpv6() { assert IPv6NetworkUtils.formatHostPort(IPV6, REST_PORT) == "[2001:db8::1]:8080" } diff --git a/utils/src/main/java/org/zstack/utils/ssh/Ssh.java b/utils/src/main/java/org/zstack/utils/ssh/Ssh.java index 69de299e258..1ae3b00aea5 100755 --- a/utils/src/main/java/org/zstack/utils/ssh/Ssh.java +++ b/utils/src/main/java/org/zstack/utils/ssh/Ssh.java @@ -345,10 +345,11 @@ public SshResult run() { @Override public String getCommand() { + String target = SshShell.formatSshTarget(username, hostname); if (download) { - return String.format("scp -P %d %s@%s:%s %s", port, username, hostname, src, dst); + return String.format("scp -P %d %s:%s %s", port, target, src, dst); } else { - return String.format("scp -P %d %s %s@%s:%s", port, src, username, hostname, dst); + return String.format("scp -P %d %s %s:%s", port, src, target, dst); } } diff --git a/utils/src/main/java/org/zstack/utils/ssh/SshShell.java b/utils/src/main/java/org/zstack/utils/ssh/SshShell.java index 80ea35c94ce..1c08aa48ab7 100755 --- a/utils/src/main/java/org/zstack/utils/ssh/SshShell.java +++ b/utils/src/main/java/org/zstack/utils/ssh/SshShell.java @@ -6,6 +6,7 @@ import org.zstack.utils.ShellUtils; import org.zstack.utils.Utils; import org.zstack.utils.logging.CLogger; +import org.zstack.utils.network.IPv6NetworkUtils; import org.zstack.utils.path.PathUtil; import java.io.File; @@ -22,6 +23,7 @@ */ public class SshShell { private static final CLogger logger = Utils.getLogger(SshShell.class); + private static final String SSH_TARGET_FORMAT = "%s@%s"; private String hostname; private String username; @@ -36,6 +38,10 @@ private void checkParams() { DebugUtils.Assert(password != null || privateKey != null, "password and privateKey must have at least one set"); } + public static String formatSshTarget(String username, String hostname) { + return String.format(SSH_TARGET_FORMAT, username, IPv6NetworkUtils.formatHostForUrl(hostname)); + } + public SshResult runCommand(String cmd) { checkParams(); String ssh; @@ -45,13 +51,13 @@ public SshResult runCommand(String cmd) { if (privateKey != null) { tempPasswordFile = File.createTempFile("zstack", "tmp"); writeSecretFile(tempPasswordFile, privateKey); - ssh = String.format("ssh -q -i %s -o UserKnownHostsFile=/dev/null -o PasswordAuthentication=no -o StrictHostKeyChecking=no -p %s %s@%s '%s'", - tempPasswordFile.getAbsolutePath(), port, username, hostname, cmd); + ssh = String.format("ssh -q -i %s -o UserKnownHostsFile=/dev/null -o PasswordAuthentication=no -o StrictHostKeyChecking=no -p %s %s '%s'", + tempPasswordFile.getAbsolutePath(), port, formatSshTarget(username, hostname), cmd); } else { tempPasswordFile = File.createTempFile("zstack", "tmp"); writeSecretFile(tempPasswordFile, password); - ssh = String.format("sshpass -f%s ssh -q -o UserKnownHostsFile=/dev/null -o PubkeyAuthentication=no -o StrictHostKeyChecking=no -p %s %s@%s '%s'", - tempPasswordFile.getAbsolutePath(), port, username, hostname, cmd); + ssh = String.format("sshpass -f%s ssh -q -o UserKnownHostsFile=/dev/null -o PubkeyAuthentication=no -o StrictHostKeyChecking=no -p %s %s '%s'", + tempPasswordFile.getAbsolutePath(), port, formatSshTarget(username, hostname), cmd); } if (logger.isTraceEnabled()) { @@ -209,4 +215,4 @@ public Boolean getWithSudo() { public void setWithSudo(Boolean withSudo) { this.withSudo = withSudo; } -} \ No newline at end of file +} From 00882ff6591e8cea1e937da6a7a00bf2b4457120 Mon Sep 17 00:00:00 2001 From: "shixin.ruan" Date: Fri, 22 May 2026 15:59:02 +0900 Subject: [PATCH 24/53] [mgt-ipv6]: bracket ceph metadata url Resolves: ZSTAC-79206 Change-Id: I4afbbd5db032c177cd901006137becac514fda8f --- .../ceph/backup/CephBackupStorageMetaDataMaker.java | 9 +++++++-- .../integration/core/ManagementNetworkIpv6Case.groovy | 9 +++++++++ 2 files changed, 16 insertions(+), 2 deletions(-) diff --git a/plugin/ceph/src/main/java/org/zstack/storage/ceph/backup/CephBackupStorageMetaDataMaker.java b/plugin/ceph/src/main/java/org/zstack/storage/ceph/backup/CephBackupStorageMetaDataMaker.java index 2b346280ce9..572f272da98 100755 --- a/plugin/ceph/src/main/java/org/zstack/storage/ceph/backup/CephBackupStorageMetaDataMaker.java +++ b/plugin/ceph/src/main/java/org/zstack/storage/ceph/backup/CephBackupStorageMetaDataMaker.java @@ -30,6 +30,7 @@ import org.zstack.utils.Utils; import org.zstack.utils.gson.JSONObjectUtil; import org.zstack.utils.logging.CLogger; +import org.zstack.utils.network.IPv6NetworkUtils; import javax.persistence.TypedQuery; import java.util.*; @@ -51,8 +52,12 @@ public class CephBackupStorageMetaDataMaker implements AddImageExtensionPoint, A @Autowired private CloudBus bus; - protected String buildUrl( String hostName, Integer monPort,String subPath) { - return String.format("http://%s:%s%s", hostName, monPort, subPath); + public static String buildAgentUrl(String hostName, Integer monPort, String subPath) { + return IPv6NetworkUtils.buildHttpUrl(hostName, monPort) + subPath; + } + + protected String buildUrl(String hostName, Integer monPort, String subPath) { + return buildAgentUrl(hostName, monPort, subPath); } @Transactional diff --git a/test/src/test/groovy/org/zstack/test/integration/core/ManagementNetworkIpv6Case.groovy b/test/src/test/groovy/org/zstack/test/integration/core/ManagementNetworkIpv6Case.groovy index 8409423dbb5..8e8c8b8af3f 100644 --- a/test/src/test/groovy/org/zstack/test/integration/core/ManagementNetworkIpv6Case.groovy +++ b/test/src/test/groovy/org/zstack/test/integration/core/ManagementNetworkIpv6Case.groovy @@ -15,6 +15,7 @@ import org.zstack.kvm.KVMHost import org.zstack.kvm.KvmHostIpmiPowerExecutor import org.zstack.network.l2.vxlan.vxlanNetworkPool.VxlanPoolApiInterceptor import org.zstack.storage.ceph.MonUri +import org.zstack.storage.ceph.backup.CephBackupStorageMetaDataMaker import org.zstack.storage.primary.nfs.NfsApiParamChecker import org.zstack.utils.URLBuilder import org.zstack.utils.ssh.SshShell @@ -85,6 +86,7 @@ class ManagementNetworkIpv6Case { testManagementServerIdPersisted() testNfsIpv6UrlParsing() testCephIpv6MonUrlParsing() + testCephMetadataAgentUrlUsesBracketedIpv6Host() testVxlanVtepIpv6Validation() testKvmExtraIpCidrSelection() testKvmIpmiAddressKeepsIpv6() @@ -292,6 +294,13 @@ class ManagementNetworkIpv6Case { assert IPv6NetworkUtils.formatHostPort(monUri.hostname, monUri.monPort) == "[${IPV6}]:6789" } + void testCephMetadataAgentUrlUsesBracketedIpv6Host() { + assert CephBackupStorageMetaDataMaker.buildAgentUrl(IPV6, REST_PORT, "/ceph/backupstorage/dumpimagemetadatatofile") == + "http://[2001:db8::1]:8080/ceph/backupstorage/dumpimagemetadatatofile" + assert CephBackupStorageMetaDataMaker.buildAgentUrl(IPV4, REST_PORT, "/ceph/backupstorage/dumpimagemetadatatofile") == + "http://192.168.1.10:8080/ceph/backupstorage/dumpimagemetadatatofile" + } + void testVxlanVtepIpv6Validation() { assert VxlanPoolApiInterceptor.isValidVtepIp(IPV4) assert VxlanPoolApiInterceptor.isValidVtepIp(IPV6) From cb90faaa735e4fa0abdedb3f03746fdf965de2f3 Mon Sep 17 00:00:00 2001 From: "shixin.ruan" Date: Fri, 22 May 2026 18:41:16 +0900 Subject: [PATCH 25/53] [mgt-ipv6]: handle zstack review comments Resolves: ZSTAC-79206 Change-Id: I807340f81675456e6a13505ffda42b89cd1f16cb --- .../main/java/org/zstack/core/Platform.java | 109 +++++++++++++----- .../VxlanPoolApiInterceptor.java | 28 +++-- .../core/ManagementNetworkIpv6Case.groovy | 19 ++- .../AddRemoteVxlanVtepIpCase.groovy | 18 +++ 4 files changed, 136 insertions(+), 38 deletions(-) diff --git a/core/src/main/java/org/zstack/core/Platform.java b/core/src/main/java/org/zstack/core/Platform.java index 0c5af5202ec..46ed530e234 100755 --- a/core/src/main/java/org/zstack/core/Platform.java +++ b/core/src/main/java/org/zstack/core/Platform.java @@ -94,6 +94,12 @@ public class Platform { private static final int IP_ADDRESS_COMMAND_MIN_TOKEN_COUNT = 2; private static final String CIDR_SEPARATOR = "/"; private static final String TEMP_FILE_SUFFIX = ".tmp"; + private static final String DATA_DIR_PROPERTY = "dataDir"; + private static final String DEFAULT_DATA_DIR = "/var/lib/zstack/"; + private static final String UNIT_TEST_ON_PROPERTY = "unitTestOn"; + private static final String JAVA_TMP_DIR_PROPERTY = "java.io.tmpdir"; + private static final String UNIT_TEST_DATA_DIR_NAME = "zstack-unit-test"; + private static final String MANAGEMENT_SERVER_ID_STATE_FILE_NAME = "management-server-id.properties"; private static final String ZSTACK_UUID_PATTERN = "[0-9a-fA-F]{32}"; private static EncryptRSA rsa = new EncryptRSA(); private static Map errorCounter = new HashMap<>(); @@ -720,14 +726,11 @@ public static String getManagementServerId() { } public static synchronized String loadOrCreateManagementServerId(File propertiesFile, Supplier idSupplier) { - Properties properties = new Properties(); - if (propertiesFile.exists()) { - try (FileInputStream inputStream = new FileInputStream(propertiesFile)) { - properties.load(inputStream); - } catch (IOException e) { - throw new CloudRuntimeException(e); - } - } + return loadOrCreateManagementServerId(propertiesFile, getManagementServerIdStateFile(), idSupplier); + } + + public static synchronized String loadOrCreateManagementServerId(File propertiesFile, File stateFile, Supplier idSupplier) { + Properties properties = loadProperties(propertiesFile); String configuredId = properties.getProperty(MANAGEMENT_SERVER_ID_PROPERTY); if (isValidManagementServerId(configuredId)) { @@ -735,17 +738,48 @@ public static synchronized String loadOrCreateManagementServerId(File properties return configuredId; } + Properties state = loadProperties(stateFile); + String persistedId = state.getProperty(MANAGEMENT_SERVER_ID_PROPERTY); + if (isValidManagementServerId(persistedId)) { + System.setProperty(MANAGEMENT_SERVER_ID_PROPERTY, persistedId); + return persistedId; + } + String generatedId = idSupplier.get(); if (!isValidManagementServerId(generatedId)) { throw new CloudRuntimeException(String.format("generated management server id[%s] is not a valid uuid", generatedId)); } - properties.setProperty(MANAGEMENT_SERVER_ID_PROPERTY, generatedId); - saveManagementServerId(propertiesFile, properties); + state.setProperty(MANAGEMENT_SERVER_ID_PROPERTY, generatedId); + saveManagementServerId(stateFile, state); System.setProperty(MANAGEMENT_SERVER_ID_PROPERTY, generatedId); return generatedId; } + private static Properties loadProperties(File file) { + Properties properties = new Properties(); + if (file.exists()) { + try (FileInputStream inputStream = new FileInputStream(file)) { + properties.load(inputStream); + } catch (IOException e) { + throw new CloudRuntimeException(e); + } + } + return properties; + } + + private static File getManagementServerIdStateFile() { + String dataDir = System.getProperty(DATA_DIR_PROPERTY); + if (dataDir == null && Boolean.parseBoolean(System.getProperty(UNIT_TEST_ON_PROPERTY))) { + dataDir = new File(System.getProperty(JAVA_TMP_DIR_PROPERTY), UNIT_TEST_DATA_DIR_NAME).getAbsolutePath(); + } + if (dataDir == null) { + dataDir = DEFAULT_DATA_DIR; + } + + return new File(dataDir, MANAGEMENT_SERVER_ID_STATE_FILE_NAME); + } + private static boolean isValidManagementServerId(String id) { if (id == null) { return false; @@ -760,6 +794,14 @@ private static boolean isValidManagementServerId(String id) { } private static void saveManagementServerId(File propertiesFile, Properties properties) { + if (propertiesFile.getParentFile() != null && !propertiesFile.getParentFile().exists()) { + try { + FileUtils.forceMkdir(propertiesFile.getParentFile()); + } catch (IOException e) { + throw new CloudRuntimeException(e); + } + } + File tmp = new File(propertiesFile.getAbsolutePath() + TEMP_FILE_SUFFIX); try (FileOutputStream outputStream = new FileOutputStream(tmp)) { properties.store(outputStream, "ZStack properties"); @@ -968,16 +1010,29 @@ private static String getManagementServerIpInternal() { } public static String getManagementServerIp6() { + return getManagementServerIpOnManagementInterface(IPv6Constants.IPv6); + } + + private static String getManagementServerIp4() { + return getManagementServerIpOnManagementInterface(IPv6Constants.IPv4); + } + + private static String getManagementServerIpOnManagementInterface(int ipVersion) { try { - Enumeration nets = NetworkInterface.getNetworkInterfaces(); - for (NetworkInterface iface : Collections.list(nets)) { - if (!iface.isUp()) { + NetworkInterface iface = findManagementServerInterface(); + if (iface == null || !iface.isUp()) { + return null; + } + + for (InetAddress address : Collections.list(iface.getInetAddresses())) { + if (address.isLoopbackAddress() || address.isLinkLocalAddress()) { continue; } - for (InetAddress ia : Collections.list(iface.getInetAddresses())) { - if (!(ia instanceof Inet4Address) && !ia.isLoopbackAddress() && !ia.isLinkLocalAddress()) { - return normalizeManagementIp(ia.getHostAddress()); - } + if (ipVersion == IPv6Constants.IPv6 && !(address instanceof Inet4Address)) { + return normalizeManagementIp(address.getHostAddress()); + } + if (ipVersion == IPv6Constants.IPv4 && address instanceof Inet4Address) { + return normalizeManagementIp(address.getHostAddress()); } } } catch (SocketException e) { @@ -987,21 +1042,15 @@ public static String getManagementServerIp6() { return null; } - private static String getManagementServerIp4() { - try { - Enumeration nets = NetworkInterface.getNetworkInterfaces(); - for (NetworkInterface iface : Collections.list(nets)) { - if (!iface.isUp()) { - continue; - } - for (InetAddress ia : Collections.list(iface.getInetAddresses())) { - if (ia instanceof Inet4Address && !ia.isLoopbackAddress() && !ia.isLinkLocalAddress()) { - return normalizeManagementIp(ia.getHostAddress()); - } + private static NetworkInterface findManagementServerInterface() throws SocketException { + String currentIp = normalizeManagementIp(getManagementServerIp()); + Enumeration nets = NetworkInterface.getNetworkInterfaces(); + for (NetworkInterface iface : Collections.list(nets)) { + for (InetAddress address : Collections.list(iface.getInetAddresses())) { + if (currentIp.equals(normalizeManagementIp(address.getHostAddress()))) { + return iface; } } - } catch (SocketException e) { - throw new CloudRuntimeException(e); } return null; diff --git a/plugin/vxlan/src/main/java/org/zstack/network/l2/vxlan/vxlanNetworkPool/VxlanPoolApiInterceptor.java b/plugin/vxlan/src/main/java/org/zstack/network/l2/vxlan/vxlanNetworkPool/VxlanPoolApiInterceptor.java index 1ed33145250..baea030dc32 100644 --- a/plugin/vxlan/src/main/java/org/zstack/network/l2/vxlan/vxlanNetworkPool/VxlanPoolApiInterceptor.java +++ b/plugin/vxlan/src/main/java/org/zstack/network/l2/vxlan/vxlanNetworkPool/VxlanPoolApiInterceptor.java @@ -17,6 +17,7 @@ import org.zstack.network.l2.vxlan.vxlanNetwork.APICreateL2VxlanNetworkMsg; import org.zstack.utils.Utils; import org.zstack.utils.logging.CLogger; +import org.zstack.utils.network.IPv6NetworkUtils; import org.zstack.utils.network.NetworkUtils; import java.util.HashMap; @@ -56,30 +57,43 @@ public APIMessage intercept(APIMessage msg) throws ApiMessageInterceptionExcepti } private void validate(APICreateVxlanPoolRemoteVtepMsg msg) { - if (!isValidVtepIp(msg.getRemoteVtepIp())) { - throw new ApiMessageInterceptionException(argerr(ORG_ZSTACK_NETWORK_L2_VXLAN_VXLANNETWORKPOOL_10032, "%s is not a valid IP address", msg.getRemoteVtepIp())); + String remoteVtepIp = normalizeVtepIp(msg.getRemoteVtepIp()); + msg.setRemoteVtepIp(remoteVtepIp); + if (!isValidVtepIp(remoteVtepIp)) { + throw new ApiMessageInterceptionException(argerr(ORG_ZSTACK_NETWORK_L2_VXLAN_VXLANNETWORKPOOL_10032, "%s is not a valid IP address", remoteVtepIp)); } SimpleQuery rqv = dbf.createQuery(VtepVO.class); rqv.add(VtepVO_.clusterUuid, SimpleQuery.Op.EQ, msg.getClusterUuid()); rqv.add(VtepVO_.poolUuid, SimpleQuery.Op.EQ, msg.getL2NetworkUuid()); - rqv.add(VtepVO_.vtepIp, SimpleQuery.Op.EQ, msg.getRemoteVtepIp()); + rqv.add(VtepVO_.vtepIp, SimpleQuery.Op.EQ, remoteVtepIp); long count = rqv.count(); if (count > 0) { - throw new ApiMessageInterceptionException(argerr(ORG_ZSTACK_NETWORK_L2_VXLAN_VXLANNETWORKPOOL_10016, "ip[%s] l2NetworkUuid[%s] clusterUuid[%s] ip exist in local vtep", msg.getRemoteVtepIp(), msg.getL2NetworkUuid(), msg.getClusterUuid())); + throw new ApiMessageInterceptionException(argerr(ORG_ZSTACK_NETWORK_L2_VXLAN_VXLANNETWORKPOOL_10016, "ip[%s] l2NetworkUuid[%s] clusterUuid[%s] ip exist in local vtep", remoteVtepIp, msg.getL2NetworkUuid(), msg.getClusterUuid())); } } private void validate(APIDeleteVxlanPoolRemoteVtepMsg msg) { - if (!isValidVtepIp(msg.getRemoteVtepIp())) { - throw new ApiMessageInterceptionException(argerr(ORG_ZSTACK_NETWORK_L2_VXLAN_VXLANNETWORKPOOL_10033, "%s is not a valid IP address", msg.getRemoteVtepIp())); + String remoteVtepIp = normalizeVtepIp(msg.getRemoteVtepIp()); + msg.setRemoteVtepIp(remoteVtepIp); + if (!isValidVtepIp(remoteVtepIp)) { + throw new ApiMessageInterceptionException(argerr(ORG_ZSTACK_NETWORK_L2_VXLAN_VXLANNETWORKPOOL_10033, "%s is not a valid IP address", remoteVtepIp)); } } public static boolean isValidVtepIp(String vtepIp) { - return NetworkUtils.isIpAddress(vtepIp); + return NetworkUtils.isIpAddress(normalizeVtepIp(vtepIp)); + } + + public static String normalizeVtepIp(String vtepIp) { + if (vtepIp == null) { + return null; + } + + String ip = vtepIp.trim(); + return IPv6NetworkUtils.isIpv6Address(ip) ? IPv6NetworkUtils.getIpv6AddressCanonicalString(ip) : ip; } private void validate(APICreateVxlanVtepMsg msg) { diff --git a/test/src/test/groovy/org/zstack/test/integration/core/ManagementNetworkIpv6Case.groovy b/test/src/test/groovy/org/zstack/test/integration/core/ManagementNetworkIpv6Case.groovy index 8e8c8b8af3f..e6541d3ff7b 100644 --- a/test/src/test/groovy/org/zstack/test/integration/core/ManagementNetworkIpv6Case.groovy +++ b/test/src/test/groovy/org/zstack/test/integration/core/ManagementNetworkIpv6Case.groovy @@ -255,22 +255,38 @@ class ManagementNetworkIpv6Case { void testManagementServerIdPersisted() { String oldValue = System.getProperty(Platform.MANAGEMENT_SERVER_ID_PROPERTY) File propertiesFile = File.createTempFile("zstack-management-server-id", ".properties") + File stateFile = File.createTempFile("zstack-management-server-id-state", ".properties") try { + System.clearProperty(Platform.MANAGEMENT_SERVER_ID_PROPERTY) propertiesFile.text = "" + stateFile.delete() String generatedId = Platform.loadOrCreateManagementServerId( propertiesFile, + stateFile, { -> MANAGEMENT_SERVER_ID } as Supplier) assert generatedId == MANAGEMENT_SERVER_ID Properties properties = new Properties() propertiesFile.withInputStream { properties.load(it) } - assert properties.getProperty(Platform.MANAGEMENT_SERVER_ID_PROPERTY) == MANAGEMENT_SERVER_ID + assert properties.getProperty(Platform.MANAGEMENT_SERVER_ID_PROPERTY) == null + Properties state = new Properties() + stateFile.withInputStream { state.load(it) } + assert state.getProperty(Platform.MANAGEMENT_SERVER_ID_PROPERTY) == MANAGEMENT_SERVER_ID String persistedId = Platform.loadOrCreateManagementServerId( propertiesFile, + stateFile, { -> NEW_MANAGEMENT_SERVER_ID } as Supplier) assert persistedId == MANAGEMENT_SERVER_ID + + propertiesFile.text = "${Platform.MANAGEMENT_SERVER_ID_PROPERTY}=${NEW_MANAGEMENT_SERVER_ID}\n" + String configuredId = Platform.loadOrCreateManagementServerId( + propertiesFile, + stateFile, + { -> MANAGEMENT_SERVER_ID } as Supplier) + assert configuredId == NEW_MANAGEMENT_SERVER_ID } finally { propertiesFile.delete() + stateFile.delete() if (oldValue == null) { System.clearProperty(Platform.MANAGEMENT_SERVER_ID_PROPERTY) } else { @@ -305,6 +321,7 @@ class ManagementNetworkIpv6Case { assert VxlanPoolApiInterceptor.isValidVtepIp(IPV4) assert VxlanPoolApiInterceptor.isValidVtepIp(IPV6) assert !VxlanPoolApiInterceptor.isValidVtepIp(INVALID_VTEP_IP) + assert VxlanPoolApiInterceptor.normalizeVtepIp(" ${IPV6_FULL}\n") == IPV6 } void testKvmExtraIpCidrSelection() { diff --git a/test/src/test/groovy/org/zstack/test/integration/network/vxlanNetwork/AddRemoteVxlanVtepIpCase.groovy b/test/src/test/groovy/org/zstack/test/integration/network/vxlanNetwork/AddRemoteVxlanVtepIpCase.groovy index 2e38c1ed1d5..1594a98bcfa 100644 --- a/test/src/test/groovy/org/zstack/test/integration/network/vxlanNetwork/AddRemoteVxlanVtepIpCase.groovy +++ b/test/src/test/groovy/org/zstack/test/integration/network/vxlanNetwork/AddRemoteVxlanVtepIpCase.groovy @@ -20,6 +20,7 @@ import org.zstack.sdk.ApiException class AddRemoteVxlanVtepIpCase extends SubCase { private static final String IPV4_REMOTE_VTEP_IP = "1.1.1.1" private static final String IPV6_REMOTE_VTEP_IP = "2001:db8:ffff::10" + private static final String IPV6_REMOTE_VTEP_FULL_IP = "2001:0db8:ffff:0000:0000:0000:0000:0010" private static final String INVALID_REMOTE_VTEP_IP = "not-a-vtep-ip" EnvSpec env @@ -150,6 +151,23 @@ class AddRemoteVxlanVtepIpCase extends SubCase { .eq(RemoteVtepVO_.vtepIp, IPV6_REMOTE_VTEP_IP) .isExists() assert Q.New(RemoteVtepVO.class).eq(RemoteVtepVO_.poolUuid, pool.uuid).count() == 1 + expect(AssertionError.class) { + createVxlanPoolRemoteVtep { + l2NetworkUuid = pool.uuid + clusterUuid = cluster.uuid + remoteVtepIp = " ${IPV6_REMOTE_VTEP_FULL_IP}\n" + } + } + deleteVxlanPoolRemoteVtep { + l2NetworkUuid = pool.uuid + clusterUuid = cluster.uuid + remoteVtepIp = " ${IPV6_REMOTE_VTEP_FULL_IP}\n" + } + assert !Q.New(RemoteVtepVO.class) + .eq(RemoteVtepVO_.poolUuid, pool.uuid) + .eq(RemoteVtepVO_.clusterUuid, cluster.uuid) + .eq(RemoteVtepVO_.vtepIp, IPV6_REMOTE_VTEP_IP) + .isExists() expect(AssertionError.class) { createVxlanPoolRemoteVtep { From 924a947764692695307fc931217ef73780846c5c Mon Sep 17 00:00:00 2001 From: "shixin.ruan" Date: Fri, 22 May 2026 20:23:57 +0900 Subject: [PATCH 26/53] [mgt-ipv6]: address review comments Keep management IPv6 cases compatible with stability tests. Resolves: ZSTAC-79206 Change-Id: I2da8cebf2d0311d56ae560c10a2e6736ad3b74da --- .../compute/host/HostApiInterceptor.java | 20 +++++++++------- .../org/zstack/core/CoreGlobalProperty.java | 3 +++ .../org/zstack/core/NetworkGlobalConfig.java | 15 ------------ .../main/java/org/zstack/core/Platform.java | 6 +---- .../appliancevm/ApplianceVmIpv6Case.groovy | 24 ++++++++++++++++++- .../core/ManagementNetworkIpv6Case.groovy | 20 +++++++++++++--- 6 files changed, 56 insertions(+), 32 deletions(-) delete mode 100644 core/src/main/java/org/zstack/core/NetworkGlobalConfig.java diff --git a/compute/src/main/java/org/zstack/compute/host/HostApiInterceptor.java b/compute/src/main/java/org/zstack/compute/host/HostApiInterceptor.java index e645f7a7bcd..e65a95ad50d 100755 --- a/compute/src/main/java/org/zstack/compute/host/HostApiInterceptor.java +++ b/compute/src/main/java/org/zstack/compute/host/HostApiInterceptor.java @@ -132,10 +132,18 @@ private void validate(APIAddHostMsg msg) { static void validateManagementEndpoint(APIAddHostMsg msg) { String managementIp = msg.getManagementIp(); - String errorCode = getManagementEndpointValidationErrorCode(managementIp); - - if (errorCode != null) { - throw new ApiMessageInterceptionException(argerr(errorCode, getManagementEndpointValidationErrorMessage(errorCode), managementIp)); + if (IPv6NetworkUtils.isIpv6Address(managementIp)) { + if (!IPv6NetworkUtils.isValidManagementIpv6Address(managementIp)) { + throw new ApiMessageInterceptionException(argerr( + ORG_ZSTACK_COMPUTE_HOST_10129, + RESERVED_MANAGEMENT_IPV6_ERROR, + managementIp)); + } + } else if (!isValidManagementEndpoint(managementIp)) { + throw new ApiMessageInterceptionException(argerr( + ORG_ZSTACK_COMPUTE_HOST_10128, + INVALID_MANAGEMENT_IP_ERROR, + managementIp)); } if (IPv6NetworkUtils.isIpv6Address(managementIp)) { @@ -151,10 +159,6 @@ static String getManagementEndpointValidationErrorCode(String managementIp) { return isValidManagementEndpoint(managementIp) ? null : ORG_ZSTACK_COMPUTE_HOST_10128; } - private static String getManagementEndpointValidationErrorMessage(String errorCode) { - return ORG_ZSTACK_COMPUTE_HOST_10129.equals(errorCode) ? RESERVED_MANAGEMENT_IPV6_ERROR : INVALID_MANAGEMENT_IP_ERROR; - } - private static boolean isValidManagementEndpoint(String endpoint) { return IPv6NetworkUtils.isValidManagementEndpoint(endpoint); } diff --git a/core/src/main/java/org/zstack/core/CoreGlobalProperty.java b/core/src/main/java/org/zstack/core/CoreGlobalProperty.java index 828b06b0bf2..d281dbb7016 100755 --- a/core/src/main/java/org/zstack/core/CoreGlobalProperty.java +++ b/core/src/main/java/org/zstack/core/CoreGlobalProperty.java @@ -79,6 +79,9 @@ public class CoreGlobalProperty { public static List CHRONY_SERVERS; @GlobalProperty(name="management.server.vip") public static String MN_VIP; + @GlobalProperty(name = "management.server.prefer.ipv6", defaultValue = "false") + @AvailableValues(value = {"true", "false"}) + public static boolean MANAGEMENT_SERVER_PREFER_IPV6; @GlobalProperty(name = "simulatorsOn", defaultValue = "false") public static boolean SIMULATORS_ON; @GlobalProperty(name = "startMode", defaultValue = "") diff --git a/core/src/main/java/org/zstack/core/NetworkGlobalConfig.java b/core/src/main/java/org/zstack/core/NetworkGlobalConfig.java deleted file mode 100644 index 1b5c78abce9..00000000000 --- a/core/src/main/java/org/zstack/core/NetworkGlobalConfig.java +++ /dev/null @@ -1,15 +0,0 @@ -package org.zstack.core; - -import org.zstack.core.config.GlobalConfig; -import org.zstack.core.config.GlobalConfigDef; -import org.zstack.core.config.GlobalConfigDefinition; -import org.zstack.core.config.GlobalConfigValidation; - -@GlobalConfigDefinition -public class NetworkGlobalConfig { - public static final String CATEGORY = "management.server"; - - @GlobalConfigValidation - @GlobalConfigDef(defaultValue = "false", type = Boolean.class, description = "Prefer IPv6 for management server address selection") - public static GlobalConfig PREFER_IPV6 = new GlobalConfig(CATEGORY, "prefer.ipv6"); -} diff --git a/core/src/main/java/org/zstack/core/Platform.java b/core/src/main/java/org/zstack/core/Platform.java index 46ed530e234..b78925704ff 100755 --- a/core/src/main/java/org/zstack/core/Platform.java +++ b/core/src/main/java/org/zstack/core/Platform.java @@ -1100,11 +1100,7 @@ public static boolean isManagementServerPreferIpv6() { return Boolean.parseBoolean(propertyValue); } - try { - return NetworkGlobalConfig.PREFER_IPV6.value(Boolean.class); - } catch (Throwable ignored) { - return false; - } + return CoreGlobalProperty.MANAGEMENT_SERVER_PREFER_IPV6; } public static String formatJGroupsInitialHosts(String nodeIp, String peerIp, int port) { diff --git a/test/src/test/groovy/org/zstack/test/integration/appliancevm/ApplianceVmIpv6Case.groovy b/test/src/test/groovy/org/zstack/test/integration/appliancevm/ApplianceVmIpv6Case.groovy index 194b0ac5e9e..62bb3b11e82 100644 --- a/test/src/test/groovy/org/zstack/test/integration/appliancevm/ApplianceVmIpv6Case.groovy +++ b/test/src/test/groovy/org/zstack/test/integration/appliancevm/ApplianceVmIpv6Case.groovy @@ -3,14 +3,36 @@ package org.zstack.test.integration.appliancevm import org.junit.Test import org.zstack.appliancevm.ApplianceVmConstant import org.zstack.appliancevm.ApplianceVmFacadeImpl +import org.zstack.testlib.SubCase -class ApplianceVmIpv6Case { +class ApplianceVmIpv6Case extends SubCase { private static final String IPV4_MN_IP = "192.168.1.10" private static final String IPV6_MN_IP = "2001:db8::1" private static final String IPV4_MN_CIDR = "192.168.1.0/24" private static final String IPV6_MN_CIDR = "2001:db8::/64" private static final String UNMATCHED_VR_CIDR = "10.0.0.0/24" + @Override + void clean() { + } + + @Override + void setup() { + } + + @Override + void environment() { + } + + @Override + void test() { + testVrBootstrapIpv6Cidr() + testVrBootstrapMnIpNoBrackets() + testVrMnIpCidrMatch() + testVrBootstrapAddressFamilyIndependent() + testVrBootstrapFallbackWhenNoCidrMatches() + } + @Test void testVrBootstrapIpv6Cidr() { Map params = buildDualStackBootstrapParams([IPV6_MN_CIDR]) diff --git a/test/src/test/groovy/org/zstack/test/integration/core/ManagementNetworkIpv6Case.groovy b/test/src/test/groovy/org/zstack/test/integration/core/ManagementNetworkIpv6Case.groovy index e6541d3ff7b..fb640bddb9e 100644 --- a/test/src/test/groovy/org/zstack/test/integration/core/ManagementNetworkIpv6Case.groovy +++ b/test/src/test/groovy/org/zstack/test/integration/core/ManagementNetworkIpv6Case.groovy @@ -3,7 +3,7 @@ package org.zstack.test.integration.core import org.zstack.appliancevm.ApplianceVmConstant import org.zstack.appliancevm.ApplianceVmFacadeImpl import org.zstack.core.ansible.AnsibleRunner -import org.zstack.core.NetworkGlobalConfig +import org.zstack.core.CoreGlobalProperty import org.zstack.core.Platform import org.zstack.core.agent.AgentManagerImpl import org.zstack.core.cloudbus.CloudBusImpl3 @@ -17,6 +17,7 @@ import org.zstack.network.l2.vxlan.vxlanNetworkPool.VxlanPoolApiInterceptor import org.zstack.storage.ceph.MonUri import org.zstack.storage.ceph.backup.CephBackupStorageMetaDataMaker import org.zstack.storage.primary.nfs.NfsApiParamChecker +import org.zstack.testlib.SubCase import org.zstack.utils.URLBuilder import org.zstack.utils.ssh.SshShell import org.zstack.utils.network.IPv6Constants @@ -26,7 +27,7 @@ import org.junit.Test import java.util.function.Supplier -class ManagementNetworkIpv6Case { +class ManagementNetworkIpv6Case extends SubCase { private static final String IPV4 = "192.168.1.10" private static final String IPV6 = "2001:db8::1" private static final String IPV6_2 = "2001:db8::2" @@ -58,6 +59,19 @@ class ManagementNetworkIpv6Case { private static final int REST_PORT = 8080 private static final int JGROUP_PORT = 7805 + @Override + void clean() { + } + + @Override + void setup() { + } + + @Override + void environment() { + } + + @Override @Test void test() { testPreferIpv6DefaultFalse() @@ -94,7 +108,7 @@ class ManagementNetworkIpv6Case { } void testPreferIpv6DefaultFalse() { - assert NetworkGlobalConfig.PREFER_IPV6.getIdentity() == "management.server.prefer.ipv6" + assert !CoreGlobalProperty.MANAGEMENT_SERVER_PREFER_IPV6 } void testPreferIpv6SystemProperty() { From 11397fd127f414aba776456c7f706ee066b6da01 Mon Sep 17 00:00:00 2001 From: "shixin.ruan" Date: Sat, 23 May 2026 19:28:37 +0900 Subject: [PATCH 27/53] [mgt-ipv6]: update generated sdk artifacts Regenerate SDK after VTEP endpoint schema changes. Resolves: ZSTAC-79206 Change-Id: I2ecf2c40b55a0f76331d95f07237dd7c79206f10 --- .../java/org/zstack/sdk/CreateVxlanPoolRemoteVtepAction.java | 4 +--- .../java/org/zstack/sdk/DeleteVxlanPoolRemoteVtepAction.java | 4 +--- 2 files changed, 2 insertions(+), 6 deletions(-) diff --git a/sdk/src/main/java/org/zstack/sdk/CreateVxlanPoolRemoteVtepAction.java b/sdk/src/main/java/org/zstack/sdk/CreateVxlanPoolRemoteVtepAction.java index 3aa0aa1b208..654399613aa 100644 --- a/sdk/src/main/java/org/zstack/sdk/CreateVxlanPoolRemoteVtepAction.java +++ b/sdk/src/main/java/org/zstack/sdk/CreateVxlanPoolRemoteVtepAction.java @@ -6,8 +6,6 @@ public class CreateVxlanPoolRemoteVtepAction extends AbstractAction { - private static final int REMOTE_VTEP_IP_MAX_LENGTH = 39; - private static final HashMap parameterMap = new HashMap<>(); private static final HashMap nonAPIParameterMap = new HashMap<>(); @@ -33,7 +31,7 @@ public Result throwExceptionIfError() { @Param(required = true, nonempty = false, nullElements = false, emptyString = true, noTrim = false) public java.lang.String clusterUuid; - @Param(required = true, maxLength = REMOTE_VTEP_IP_MAX_LENGTH, nonempty = false, nullElements = false, emptyString = true, noTrim = false) + @Param(required = true, maxLength = 39, nonempty = false, nullElements = false, emptyString = true, noTrim = false) public java.lang.String remoteVtepIp; @Param(required = false) diff --git a/sdk/src/main/java/org/zstack/sdk/DeleteVxlanPoolRemoteVtepAction.java b/sdk/src/main/java/org/zstack/sdk/DeleteVxlanPoolRemoteVtepAction.java index dd2c210c24b..281e0870889 100644 --- a/sdk/src/main/java/org/zstack/sdk/DeleteVxlanPoolRemoteVtepAction.java +++ b/sdk/src/main/java/org/zstack/sdk/DeleteVxlanPoolRemoteVtepAction.java @@ -6,8 +6,6 @@ public class DeleteVxlanPoolRemoteVtepAction extends AbstractAction { - private static final int REMOTE_VTEP_IP_MAX_LENGTH = 39; - private static final HashMap parameterMap = new HashMap<>(); private static final HashMap nonAPIParameterMap = new HashMap<>(); @@ -33,7 +31,7 @@ public Result throwExceptionIfError() { @Param(required = true, nonempty = false, nullElements = false, emptyString = true, noTrim = false) public java.lang.String clusterUuid; - @Param(required = true, maxLength = REMOTE_VTEP_IP_MAX_LENGTH, nonempty = false, nullElements = false, emptyString = true, noTrim = false) + @Param(required = true, maxLength = 39, nonempty = false, nullElements = false, emptyString = true, noTrim = false) public java.lang.String remoteVtepIp; @Param(required = false) From ad16cc76d9cb970ea53006285c3eae76e43bb039 Mon Sep 17 00:00:00 2001 From: "shixin.ruan" Date: Sun, 24 May 2026 10:44:42 +0900 Subject: [PATCH 28/53] [mgt-ipv6]: skip intentional must-fail case Mark the framework failure sentinel case as skipped. It should not be collected by normal UnitTest suites. Related: ZSTAC-79206 Change-Id: I11397fd127f414aba776456c7f706ee066b6da01 --- .../groovy/org/zstack/test/integration/core/MustFailCase.groovy | 2 ++ 1 file changed, 2 insertions(+) diff --git a/test/src/test/groovy/org/zstack/test/integration/core/MustFailCase.groovy b/test/src/test/groovy/org/zstack/test/integration/core/MustFailCase.groovy index a502ed44985..c8f5387b9de 100644 --- a/test/src/test/groovy/org/zstack/test/integration/core/MustFailCase.groovy +++ b/test/src/test/groovy/org/zstack/test/integration/core/MustFailCase.groovy @@ -1,10 +1,12 @@ package org.zstack.test.integration.core +import org.zstack.testlib.SkipTestSuite import org.zstack.testlib.SubCase /** * Created by lining on 2017/4/10. */ +@SkipTestSuite class MustFailCase extends SubCase { @Override From b13e29993202fa40003b769058ee4f69c363073c Mon Sep 17 00:00:00 2001 From: "shixin.ruan" Date: Wed, 27 May 2026 12:34:59 +0900 Subject: [PATCH 29/53] [core]: register IPv6 prefer config Register management.server/prefer.ipv6 as a GlobalConfig so QueryGlobalConfig and runtime updates work. Keep the startup system property override and fall back to the legacy global property before GlobalConfig is available. Resolves: ZSTAC-85520 Change-Id: Ia59515ce9a9a29eeecb9b5ab7d83057823089884 --- .../core/ManagementServerGlobalConfig.java | 16 +++++++++++ .../main/java/org/zstack/core/Platform.java | 6 ++++- .../core/ManagementNetworkIpv6Case.groovy | 27 +++++++++++++++++++ 3 files changed, 48 insertions(+), 1 deletion(-) create mode 100644 core/src/main/java/org/zstack/core/ManagementServerGlobalConfig.java diff --git a/core/src/main/java/org/zstack/core/ManagementServerGlobalConfig.java b/core/src/main/java/org/zstack/core/ManagementServerGlobalConfig.java new file mode 100644 index 00000000000..d72c766497f --- /dev/null +++ b/core/src/main/java/org/zstack/core/ManagementServerGlobalConfig.java @@ -0,0 +1,16 @@ +package org.zstack.core; + +import org.zstack.core.config.GlobalConfig; +import org.zstack.core.config.GlobalConfigDef; +import org.zstack.core.config.GlobalConfigDefinition; +import org.zstack.core.config.GlobalConfigValidation; + +@GlobalConfigDefinition +public class ManagementServerGlobalConfig { + public static final String CATEGORY = "management.server"; + + @GlobalConfigDef(defaultValue = "false", type = Boolean.class, + description = "Prefer IPv6 when selecting the management server IP on dual-stack hosts") + @GlobalConfigValidation(validValues = {"true", "false"}) + public static GlobalConfig PREFER_IPV6 = new GlobalConfig(CATEGORY, "prefer.ipv6"); +} diff --git a/core/src/main/java/org/zstack/core/Platform.java b/core/src/main/java/org/zstack/core/Platform.java index b78925704ff..cb20cc3bb4a 100755 --- a/core/src/main/java/org/zstack/core/Platform.java +++ b/core/src/main/java/org/zstack/core/Platform.java @@ -1100,7 +1100,11 @@ public static boolean isManagementServerPreferIpv6() { return Boolean.parseBoolean(propertyValue); } - return CoreGlobalProperty.MANAGEMENT_SERVER_PREFER_IPV6; + try { + return ManagementServerGlobalConfig.PREFER_IPV6.value(Boolean.class); + } catch (RuntimeException e) { + return CoreGlobalProperty.MANAGEMENT_SERVER_PREFER_IPV6; + } } public static String formatJGroupsInitialHosts(String nodeIp, String peerIp, int port) { diff --git a/test/src/test/groovy/org/zstack/test/integration/core/ManagementNetworkIpv6Case.groovy b/test/src/test/groovy/org/zstack/test/integration/core/ManagementNetworkIpv6Case.groovy index fb640bddb9e..3ecd991d04d 100644 --- a/test/src/test/groovy/org/zstack/test/integration/core/ManagementNetworkIpv6Case.groovy +++ b/test/src/test/groovy/org/zstack/test/integration/core/ManagementNetworkIpv6Case.groovy @@ -4,6 +4,7 @@ import org.zstack.appliancevm.ApplianceVmConstant import org.zstack.appliancevm.ApplianceVmFacadeImpl import org.zstack.core.ansible.AnsibleRunner import org.zstack.core.CoreGlobalProperty +import org.zstack.core.ManagementServerGlobalConfig import org.zstack.core.Platform import org.zstack.core.agent.AgentManagerImpl import org.zstack.core.cloudbus.CloudBusImpl3 @@ -75,6 +76,8 @@ class ManagementNetworkIpv6Case extends SubCase { @Test void test() { testPreferIpv6DefaultFalse() + testPreferIpv6GlobalConfigDefinition() + testPreferIpv6GlobalConfigValue() testPreferIpv6SystemProperty() testSelectManagementServerIpDualStackPolicy() testSelectManagementServerIpSkipsLoopbackAndLinkLocal() @@ -111,6 +114,30 @@ class ManagementNetworkIpv6Case extends SubCase { assert !CoreGlobalProperty.MANAGEMENT_SERVER_PREFER_IPV6 } + void testPreferIpv6GlobalConfigDefinition() { + assert ManagementServerGlobalConfig.PREFER_IPV6.category == "management.server" + assert ManagementServerGlobalConfig.PREFER_IPV6.name == "prefer.ipv6" + } + + void testPreferIpv6GlobalConfigValue() { + String oldPropertyValue = System.getProperty("management.server.prefer.ipv6") + String oldGlobalConfigValue = ManagementServerGlobalConfig.PREFER_IPV6.@value + try { + System.clearProperty("management.server.prefer.ipv6") + ManagementServerGlobalConfig.PREFER_IPV6.@value = "true" + assert Platform.isManagementServerPreferIpv6() + ManagementServerGlobalConfig.PREFER_IPV6.@value = "false" + assert !Platform.isManagementServerPreferIpv6() + } finally { + if (oldPropertyValue == null) { + System.clearProperty("management.server.prefer.ipv6") + } else { + System.setProperty("management.server.prefer.ipv6", oldPropertyValue) + } + ManagementServerGlobalConfig.PREFER_IPV6.@value = oldGlobalConfigValue + } + } + void testPreferIpv6SystemProperty() { String oldValue = System.getProperty("management.server.prefer.ipv6") try { From 04d65fcd90fbfa87a188ac30657325d2721a6ff0 Mon Sep 17 00:00:00 2001 From: "shixin.ruan" Date: Wed, 27 May 2026 12:56:47 +0900 Subject: [PATCH 30/53] [utils]: fix IPv6 ssh target Use bare IPv6 for ssh command targets. Keep bracketed IPv6 only for scp host:path syntax. Resolves: ZSTAC-85522 Change-Id: I0d655ccf654634edb109f9c8025a7b70dbf34da4 --- .../test/integration/core/ManagementNetworkIpv6Case.groovy | 5 +++-- utils/src/main/java/org/zstack/utils/ssh/Ssh.java | 2 +- utils/src/main/java/org/zstack/utils/ssh/SshShell.java | 4 ++++ 3 files changed, 8 insertions(+), 3 deletions(-) diff --git a/test/src/test/groovy/org/zstack/test/integration/core/ManagementNetworkIpv6Case.groovy b/test/src/test/groovy/org/zstack/test/integration/core/ManagementNetworkIpv6Case.groovy index 3ecd991d04d..19f173f083a 100644 --- a/test/src/test/groovy/org/zstack/test/integration/core/ManagementNetworkIpv6Case.groovy +++ b/test/src/test/groovy/org/zstack/test/integration/core/ManagementNetworkIpv6Case.groovy @@ -234,9 +234,10 @@ class ManagementNetworkIpv6Case extends SubCase { "http://[2001:db8::1]:8080/zstack${RESTConstant.COMMAND_CHANNEL_PATH}" } - void testSshTargetUsesBracketedIpv6Host() { + void testSshAndScpTargetsFormatIpv6Host() { assert SshShell.formatSshTarget("root", IPV4) == "root@192.168.1.10" - assert SshShell.formatSshTarget("root", IPV6) == "root@[2001:db8::1]" + assert SshShell.formatSshTarget("root", IPV6) == "root@2001:db8::1" + assert SshShell.formatScpTarget("root", IPV6) == "root@[2001:db8::1]" assert SshShell.formatSshTarget("root", "host-01.example.com") == "root@host-01.example.com" } diff --git a/utils/src/main/java/org/zstack/utils/ssh/Ssh.java b/utils/src/main/java/org/zstack/utils/ssh/Ssh.java index 1ae3b00aea5..3f61ae0b2f8 100755 --- a/utils/src/main/java/org/zstack/utils/ssh/Ssh.java +++ b/utils/src/main/java/org/zstack/utils/ssh/Ssh.java @@ -345,7 +345,7 @@ public SshResult run() { @Override public String getCommand() { - String target = SshShell.formatSshTarget(username, hostname); + String target = SshShell.formatScpTarget(username, hostname); if (download) { return String.format("scp -P %d %s:%s %s", port, target, src, dst); } else { diff --git a/utils/src/main/java/org/zstack/utils/ssh/SshShell.java b/utils/src/main/java/org/zstack/utils/ssh/SshShell.java index 1c08aa48ab7..9bb2c822a98 100755 --- a/utils/src/main/java/org/zstack/utils/ssh/SshShell.java +++ b/utils/src/main/java/org/zstack/utils/ssh/SshShell.java @@ -39,6 +39,10 @@ private void checkParams() { } public static String formatSshTarget(String username, String hostname) { + return String.format(SSH_TARGET_FORMAT, username, hostname); + } + + public static String formatScpTarget(String username, String hostname) { return String.format(SSH_TARGET_FORMAT, username, IPv6NetworkUtils.formatHostForUrl(hostname)); } From e50ef994c807897e2fbbb6c56713cc2dca8850a5 Mon Sep 17 00:00:00 2001 From: "shixin.ruan" Date: Wed, 27 May 2026 16:42:10 +0900 Subject: [PATCH 31/53] [vrouter]: select callback IP for VR Use a callback URL in the same IP family as the virtual router management NIC. This keeps IPv4-only vrouter agents reachable when MN is dual-stack and its default management IP is IPv6. Resolves: ZSTAC-85527 Change-Id: Iaa630fc59d30c2675db7bbe47cb1b7c8d58bb023 --- .../org/zstack/core/rest/RESTFacadeImpl.java | 5 ++++ .../org/zstack/header/rest/RESTFacade.java | 2 ++ .../service/virtualrouter/VirtualRouter.java | 6 ++-- .../virtualrouter/VirtualRouterManager.java | 3 ++ .../VirtualRouterManagerImpl.java | 28 +++++++++++++++++++ .../VirtualRouterDeployAgentFlow.java | 4 +-- .../virtualrouter/vyos/VyosConnectFlow.java | 2 +- 7 files changed, 45 insertions(+), 5 deletions(-) diff --git a/core/src/main/java/org/zstack/core/rest/RESTFacadeImpl.java b/core/src/main/java/org/zstack/core/rest/RESTFacadeImpl.java index 41f1354ceb1..e4ae416ff42 100755 --- a/core/src/main/java/org/zstack/core/rest/RESTFacadeImpl.java +++ b/core/src/main/java/org/zstack/core/rest/RESTFacadeImpl.java @@ -986,6 +986,11 @@ public String getCallbackUrl() { return callbackUrl; } + @Override + public String buildCallbackUrl(String hostName) { + return buildCallbackUrl(hostName, port, path); + } + @Override public String getHostName() { return callbackHostName; diff --git a/header/src/main/java/org/zstack/header/rest/RESTFacade.java b/header/src/main/java/org/zstack/header/rest/RESTFacade.java index e9d3120f9d6..31f4ac15f4e 100755 --- a/header/src/main/java/org/zstack/header/rest/RESTFacade.java +++ b/header/src/main/java/org/zstack/header/rest/RESTFacade.java @@ -88,6 +88,8 @@ public interface RESTFacade { String getCallbackUrl(); + String buildCallbackUrl(String hostName); + String getHostName(); int getPort(); 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..e896b0f664a 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 @@ -187,7 +187,8 @@ protected void handleLocalMessage(Message msg) { void doPing(String vrUuid, ReturnValueCompletion completion) { PingCmd cmd = new PingCmd(); cmd.setUuid(vrUuid); - restf.asyncJsonPost(buildUrl(vr.getManagementNic().getIp(), VirtualRouterConstant.VR_PING), cmd, new JsonAsyncRESTCallback(completion) { + restf.asyncJsonPost(buildUrl(vr.getManagementNic().getIp(), VirtualRouterConstant.VR_PING), + cmd, vrMgr.buildAgentCallbackUrlHeaders(vr.getManagementNic().getIp()), new JsonAsyncRESTCallback(completion) { @Override public void fail(ErrorCode err) { completion.fail(err); @@ -685,7 +686,8 @@ public void run(final SyncTaskChain chain) { self.getUuid(), msg.getPath())); } - restf.asyncJsonPost(buildUrl(vr.getManagementNic().getIp(), msg.getPath()), msg.getCommand(), new JsonAsyncRESTCallback(msg, chain) { + restf.asyncJsonPost(buildUrl(vr.getManagementNic().getIp(), msg.getPath()), + msg.getCommand(), vrMgr.buildAgentCallbackUrlHeaders(vr.getManagementNic().getIp()), new JsonAsyncRESTCallback(msg, chain) { @Override public void fail(ErrorCode err) { reply.setError(err); diff --git a/plugin/virtualRouterProvider/src/main/java/org/zstack/network/service/virtualrouter/VirtualRouterManager.java b/plugin/virtualRouterProvider/src/main/java/org/zstack/network/service/virtualrouter/VirtualRouterManager.java index a7711086975..17a790f1020 100755 --- a/plugin/virtualRouterProvider/src/main/java/org/zstack/network/service/virtualrouter/VirtualRouterManager.java +++ b/plugin/virtualRouterProvider/src/main/java/org/zstack/network/service/virtualrouter/VirtualRouterManager.java @@ -11,6 +11,7 @@ import org.zstack.header.vm.VmNicInventory; import java.util.List; +import java.util.Map; public interface VirtualRouterManager { @@ -18,6 +19,8 @@ public interface VirtualRouterManager { String buildUrl(String mgmtNicIp, String subPath); + Map buildAgentCallbackUrlHeaders(String mgmtNicIp); + List selectL3NetworksNeedingSpecificNetworkService(List candidate, NetworkServiceType nsType); List selectGuestL3NetworksNeedingSpecificNetworkService(List candidate, NetworkServiceType nsType, String publicUuid); 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 a876ee24a34..3e435674ae7 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 @@ -50,6 +50,8 @@ import org.zstack.header.query.ExpandedQueryAliasStruct; import org.zstack.header.query.ExpandedQueryStruct; import org.zstack.header.query.QueryBelongFilter; +import org.zstack.header.rest.RESTConstant; +import org.zstack.header.rest.RESTFacade; import org.zstack.header.tag.*; import org.zstack.header.vm.*; import org.zstack.identity.Account; @@ -117,6 +119,9 @@ public class VirtualRouterManagerImpl extends AbstractService implements Virtual private final Map hypervisorBackends = new HashMap(); private final Map vrParallelismDegrees = new ConcurrentHashMap(); + @Autowired + private RESTFacade restf; + private List virtualRouterPostCreateFlows; private List virtualRouterPostStartFlows; private List virtualRouterPostRebootFlows; @@ -967,6 +972,29 @@ public String buildUrl(String mgmtNicIp, String subPath) { return ub.build().toUriString(); } + @Override + public Map buildAgentCallbackUrlHeaders(String mgmtNicIp) { + return Collections.singletonMap(RESTConstant.CALLBACK_URL, restf.buildCallbackUrl(selectManagementIpForAgent(mgmtNicIp))); + } + + private String selectManagementIpForAgent(String agentIp) { + if (IPv6NetworkUtils.isIpv6Address(agentIp)) { + return Platform.getManagementServerIps().stream() + .filter(IPv6NetworkUtils::isIpv6Address) + .findFirst() + .orElse(Platform.getManagementServerIp()); + } + + if (NetworkUtils.isIpv4Address(agentIp)) { + return Platform.getManagementServerIps().stream() + .filter(NetworkUtils::isIpv4Address) + .findFirst() + .orElse(Platform.getManagementServerIp()); + } + + return Platform.getManagementServerIp(); + } + private void buildWorkFlowBuilder() { postCreateFlowsBuilder = FlowChainBuilder.newBuilder().setFlowClassNames(virtualRouterPostCreateFlows).construct(); postStartFlowsBuilder = FlowChainBuilder.newBuilder().setFlowClassNames(virtualRouterPostStartFlows).construct(); diff --git a/plugin/virtualRouterProvider/src/main/java/org/zstack/network/service/virtualrouter/lifecycle/VirtualRouterDeployAgentFlow.java b/plugin/virtualRouterProvider/src/main/java/org/zstack/network/service/virtualrouter/lifecycle/VirtualRouterDeployAgentFlow.java index 903f53d18d7..0b6e0617a14 100755 --- a/plugin/virtualRouterProvider/src/main/java/org/zstack/network/service/virtualrouter/lifecycle/VirtualRouterDeployAgentFlow.java +++ b/plugin/virtualRouterProvider/src/main/java/org/zstack/network/service/virtualrouter/lifecycle/VirtualRouterDeployAgentFlow.java @@ -110,7 +110,7 @@ public void run(final FlowTrigger trigger, Map data) { cmd.setUuid(vr.getUuid()); cmd.setRestartDnsmasqAfterNumberOfSIGUSER1(VirtualRouterGlobalConfig.RESTART_DNSMASQ_COUNT.value(Integer.class)); if (timeout == null) { - restf.asyncJsonPost(url, cmd, new JsonAsyncRESTCallback(trigger) { + restf.asyncJsonPost(url, cmd, vrMgr.buildAgentCallbackUrlHeaders(mgmtNic.getIp()), new JsonAsyncRESTCallback(trigger) { @Override public void fail(ErrorCode err) { trigger.fail(err); @@ -131,7 +131,7 @@ public Class getReturnClass() { } }); } else { - restf.asyncJsonPost(url, cmd, new JsonAsyncRESTCallback(trigger) { + restf.asyncJsonPost(url, cmd, vrMgr.buildAgentCallbackUrlHeaders(mgmtNic.getIp()), new JsonAsyncRESTCallback(trigger) { @Override public void fail(ErrorCode err) { trigger.fail(err); diff --git a/plugin/virtualRouterProvider/src/main/java/org/zstack/network/service/virtualrouter/vyos/VyosConnectFlow.java b/plugin/virtualRouterProvider/src/main/java/org/zstack/network/service/virtualrouter/vyos/VyosConnectFlow.java index b3a7037e4cc..26f1caf8f76 100755 --- a/plugin/virtualRouterProvider/src/main/java/org/zstack/network/service/virtualrouter/vyos/VyosConnectFlow.java +++ b/plugin/virtualRouterProvider/src/main/java/org/zstack/network/service/virtualrouter/vyos/VyosConnectFlow.java @@ -188,7 +188,7 @@ public void run(final FlowTrigger trigger, Map data) { cmd.setParms(parms); - restf.asyncJsonPost(url, cmd, new JsonAsyncRESTCallback(trigger) { + restf.asyncJsonPost(url, cmd, vrMgr.buildAgentCallbackUrlHeaders(mgmtNic.getIp()), new JsonAsyncRESTCallback(trigger) { @Override public void fail(ErrorCode err) { errs.add(err); From 93cce8bd3082c9d1afac841500a9710be359932c Mon Sep 17 00:00:00 2001 From: "shixin.ruan" Date: Wed, 27 May 2026 18:51:51 +0900 Subject: [PATCH 32/53] [console]: listen on IPv6 proxy host Console proxy returned the MN IPv6 address to clients but selected the listen address from agentIp, which is 127.0.0.1 for the management-node agent. Use the client-facing proxy hostname to choose the wildcard listen address so IPv6 console URLs are reachable. Resolves: ZSTAC-85595 Change-Id: Ief18c9b847f0e0c050ce993a50244533614fd2b8 --- .../src/main/java/org/zstack/console/ConsoleProxyBase.java | 6 +++--- .../test/integration/core/ManagementNetworkIpv6Case.groovy | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/console/src/main/java/org/zstack/console/ConsoleProxyBase.java b/console/src/main/java/org/zstack/console/ConsoleProxyBase.java index 92f3a77b036..bf742b34ec5 100755 --- a/console/src/main/java/org/zstack/console/ConsoleProxyBase.java +++ b/console/src/main/java/org/zstack/console/ConsoleProxyBase.java @@ -82,7 +82,7 @@ private void doEstablishConsoleProxyConnection(ConsoleUrl consoleUrl, final Retu cmd.setTargetSchema(targetSchema); cmd.setTargetHostname(targetHostname); cmd.setTargetPort(targetPort); - cmd.setProxyHostname(selectProxyListenHostname(self.getAgentIp())); + cmd.setProxyHostname(selectProxyListenHostname(self.getProxyHostname())); if (ConsoleConstants.HTTP_SCHEMA.equals(targetSchema)) { cmd.setProxyPort(CoreGlobalProperty.HTTP_CONSOLE_PROXY_PORT); } else { @@ -128,8 +128,8 @@ public Class getReturnClass() { }); } - public static String selectProxyListenHostname(String agentIp) { - return IPv6NetworkUtils.isIpv6Address(agentIp) ? ANY_IPV6_ADDRESS : ANY_IPV4_ADDRESS; + public static String selectProxyListenHostname(String proxyHostname) { + return IPv6NetworkUtils.isIpv6Address(proxyHostname) ? ANY_IPV6_ADDRESS : ANY_IPV4_ADDRESS; } void doEstablishDirectConsoleConnection(ConsoleUrl consoleUrl, final ReturnValueCompletion completion) { diff --git a/test/src/test/groovy/org/zstack/test/integration/core/ManagementNetworkIpv6Case.groovy b/test/src/test/groovy/org/zstack/test/integration/core/ManagementNetworkIpv6Case.groovy index 19f173f083a..79185aecd6c 100644 --- a/test/src/test/groovy/org/zstack/test/integration/core/ManagementNetworkIpv6Case.groovy +++ b/test/src/test/groovy/org/zstack/test/integration/core/ManagementNetworkIpv6Case.groovy @@ -86,7 +86,7 @@ class ManagementNetworkIpv6Case extends SubCase { testBuildUrlIpv6() testLegacyUrlBuilderIpv6() testConsoleVncUriIpv6() - testConsoleProxyListenHostByAgentIpVersion() + testConsoleProxyListenHostByProxyIpVersion() testCoreManagementUrlsIpv6() testRestFacadeIpv6Urls() testSshTargetUsesBracketedIpv6Host() @@ -213,7 +213,7 @@ class ManagementNetworkIpv6Case extends SubCase { assert uri.port == REST_PORT } - void testConsoleProxyListenHostByAgentIpVersion() { + void testConsoleProxyListenHostByProxyIpVersion() { assert ConsoleProxyBase.selectProxyListenHostname(IPV6) == "::" assert ConsoleProxyBase.selectProxyListenHostname(IPV4) == "0.0.0.0" assert ConsoleProxyBase.selectProxyListenHostname("mn.example.com") == "0.0.0.0" From 98937282975bc86265cfc8815a52d538e3baed12 Mon Sep 17 00:00:00 2001 From: "shixin.ruan" Date: Thu, 28 May 2026 01:17:31 +0900 Subject: [PATCH 33/53] [mgt-ipv6]: fix ssh and callback ipv6 paths Fixes ZSTAC-85605 and ZSTAC-85612. SSH keeps raw IPv6 hosts while SCP brackets IPv6 paths. Ansible callback checker now passes -6 for IPv6 nc and nmap. Hotfix verified on 172.24.249.182. Resolves: ZSTAC-79206 Change-Id: Iaa7204e638335c7bf1496b2cd5e0314081e598cb --- .../core/ansible/CallBackNetworkChecker.java | 17 +++++++++--- .../core/ManagementNetworkIpv6Case.groovy | 26 ++++++++++++++++--- .../utils/network/IPv6NetworkUtils.java | 12 +++++++++ .../java/org/zstack/utils/ssh/SshShell.java | 14 +++++----- 4 files changed, 56 insertions(+), 13 deletions(-) diff --git a/core/src/main/java/org/zstack/core/ansible/CallBackNetworkChecker.java b/core/src/main/java/org/zstack/core/ansible/CallBackNetworkChecker.java index 031f624218c..5cf5665c06b 100644 --- a/core/src/main/java/org/zstack/core/ansible/CallBackNetworkChecker.java +++ b/core/src/main/java/org/zstack/core/ansible/CallBackNetworkChecker.java @@ -10,6 +10,7 @@ import org.zstack.utils.ssh.SshCmdHelper; import org.zstack.utils.ssh.SshException; import org.zstack.utils.ssh.SshResult; +import org.zstack.utils.network.IPv6NetworkUtils; import static org.zstack.core.Platform.operr; import static org.zstack.utils.StringDSL.ln; @@ -30,8 +31,11 @@ public class CallBackNetworkChecker implements AnsibleChecker { private String callbackIp = Platform.getManagementServerIp(); private int callBackPort = Platform.getManagementNodeServicePort(); - private static StringDSL.StringWrapper script = ln( - "cat /dev/null | nc {2} {1} || echo {0} | sudo -S nmap -sS -P0 -n -p {1} {2} 2>/dev/null | grep \"1 host up\"" + private static final String EMPTY_COMMAND_OPTION = ""; + private static final String IPV6_COMMAND_OPTION = "-6 "; + private static final String HOST_UP_PATTERN = "1 host up"; + private static final StringDSL.StringWrapper CALLBACK_CHECK_SCRIPT = ln( + "cat /dev/null | nc {3}{2} {1} || echo {0} | sudo -S nmap {4}-sS -P0 -n -p {1} {2} 2>/dev/null | grep \"{5}\"" ); @Override @@ -49,7 +53,7 @@ public void deleteDestFile() { * if failed, use nmap to try again. */ private ErrorCode useNcatAndNmapToTestConnection(Ssh ssh) { - String srcScript = script.format(SshCmdHelper.shellQuote(password), callBackPort, callbackIp); + String srcScript = buildCallbackCheckScript(SshCmdHelper.shellQuote(password), callBackPort, callbackIp); ssh.sudoCommand(srcScript); SshResult ret = ssh.run(); @@ -58,6 +62,13 @@ private ErrorCode useNcatAndNmapToTestConnection(Ssh ssh) { return null; } + public static String buildCallbackCheckScript(String password, int port, String callbackIp) { + String callbackHost = IPv6NetworkUtils.stripHostUrlBrackets(callbackIp); + String ipVersionOption = IPv6NetworkUtils.isIpv6Address(callbackHost) ? IPV6_COMMAND_OPTION : EMPTY_COMMAND_OPTION; + + return CALLBACK_CHECK_SCRIPT.format(password, port, callbackHost, ipVersionOption, ipVersionOption, HOST_UP_PATTERN); + } + @Override public ErrorCode stopAnsible() { if (CoreGlobalProperty.UNIT_TEST_ON || !AnsibleGlobalConfig.CHECK_MANAGEMENT_CALLBACK.value(Boolean.class)) { diff --git a/test/src/test/groovy/org/zstack/test/integration/core/ManagementNetworkIpv6Case.groovy b/test/src/test/groovy/org/zstack/test/integration/core/ManagementNetworkIpv6Case.groovy index 79185aecd6c..ed41c0e44e7 100644 --- a/test/src/test/groovy/org/zstack/test/integration/core/ManagementNetworkIpv6Case.groovy +++ b/test/src/test/groovy/org/zstack/test/integration/core/ManagementNetworkIpv6Case.groovy @@ -2,6 +2,7 @@ package org.zstack.test.integration.core import org.zstack.appliancevm.ApplianceVmConstant import org.zstack.appliancevm.ApplianceVmFacadeImpl +import org.zstack.core.ansible.CallBackNetworkChecker import org.zstack.core.ansible.AnsibleRunner import org.zstack.core.CoreGlobalProperty import org.zstack.core.ManagementServerGlobalConfig @@ -89,7 +90,9 @@ class ManagementNetworkIpv6Case extends SubCase { testConsoleProxyListenHostByProxyIpVersion() testCoreManagementUrlsIpv6() testRestFacadeIpv6Urls() - testSshTargetUsesBracketedIpv6Host() + testSshTargetUsesRawIpv6Host() + testScpTargetUsesBracketedIpv6Host() + testCallbackCheckerUsesIpv6Options() testBuildHostPortIpv6() testBracketIpv6Idempotent() testNormalizeIpv6() @@ -234,13 +237,29 @@ class ManagementNetworkIpv6Case extends SubCase { "http://[2001:db8::1]:8080/zstack${RESTConstant.COMMAND_CHANNEL_PATH}" } - void testSshAndScpTargetsFormatIpv6Host() { + void testSshTargetUsesRawIpv6Host() { assert SshShell.formatSshTarget("root", IPV4) == "root@192.168.1.10" assert SshShell.formatSshTarget("root", IPV6) == "root@2001:db8::1" - assert SshShell.formatScpTarget("root", IPV6) == "root@[2001:db8::1]" + assert SshShell.formatSshTarget("root", "[2001:db8::1]") == "root@2001:db8::1" assert SshShell.formatSshTarget("root", "host-01.example.com") == "root@host-01.example.com" } + void testScpTargetUsesBracketedIpv6Host() { + assert SshShell.formatScpTarget("root", IPV4) == "root@192.168.1.10" + assert SshShell.formatScpTarget("root", IPV6) == "root@[2001:db8::1]" + assert SshShell.formatScpTarget("root", "host-01.example.com") == "root@host-01.example.com" + } + + void testCallbackCheckerUsesIpv6Options() { + String ipv4Script = CallBackNetworkChecker.buildCallbackCheckScript("password", REST_PORT, IPV4) + assert ipv4Script.contains("nc ${IPV4} ${REST_PORT}") + assert ipv4Script.contains("nmap -sS -P0 -n -p ${REST_PORT} ${IPV4}") + + String ipv6Script = CallBackNetworkChecker.buildCallbackCheckScript("password", REST_PORT, IPV6) + assert ipv6Script.contains("nc -6 ${IPV6} ${REST_PORT}") + assert ipv6Script.contains("nmap -6 -sS -P0 -n -p ${REST_PORT} ${IPV6}") + } + void testBuildHostPortIpv6() { assert IPv6NetworkUtils.formatHostPort(IPV6, REST_PORT) == "[2001:db8::1]:8080" } @@ -248,6 +267,7 @@ class ManagementNetworkIpv6Case extends SubCase { void testBracketIpv6Idempotent() { assert IPv6NetworkUtils.formatHostForUrl(IPV6) == "[2001:db8::1]" assert IPv6NetworkUtils.formatHostForUrl("[2001:db8::1]") == "[2001:db8::1]" + assert IPv6NetworkUtils.stripHostUrlBrackets("[2001:db8::1]") == IPV6 } void testNormalizeIpv6() { diff --git a/utils/src/main/java/org/zstack/utils/network/IPv6NetworkUtils.java b/utils/src/main/java/org/zstack/utils/network/IPv6NetworkUtils.java index 5acc04ab0cb..355e43d3fe6 100644 --- a/utils/src/main/java/org/zstack/utils/network/IPv6NetworkUtils.java +++ b/utils/src/main/java/org/zstack/utils/network/IPv6NetworkUtils.java @@ -530,6 +530,18 @@ public static String formatHostForUrl(String host) { return isIpv6Address(host) ? String.format(URL_IPV6_HOST_FORMAT, host) : host; } + public static String stripHostUrlBrackets(String host) { + if (host == null) { + return null; + } + + if (host.startsWith(IPV6_BRACKET_PREFIX) && host.endsWith(IPV6_BRACKET_SUFFIX)) { + return host.substring(IPV6_BRACKET_PREFIX.length(), host.length() - IPV6_BRACKET_SUFFIX.length()); + } + + return host; + } + public static String buildHttpUrl(String host, int port) { return String.format(HTTP_URL_FORMAT, formatHostForUrl(host), port); } diff --git a/utils/src/main/java/org/zstack/utils/ssh/SshShell.java b/utils/src/main/java/org/zstack/utils/ssh/SshShell.java index 9bb2c822a98..cde850eb1b8 100755 --- a/utils/src/main/java/org/zstack/utils/ssh/SshShell.java +++ b/utils/src/main/java/org/zstack/utils/ssh/SshShell.java @@ -39,7 +39,7 @@ private void checkParams() { } public static String formatSshTarget(String username, String hostname) { - return String.format(SSH_TARGET_FORMAT, username, hostname); + return String.format(SSH_TARGET_FORMAT, username, IPv6NetworkUtils.stripHostUrlBrackets(hostname)); } public static String formatScpTarget(String username, String hostname) { @@ -99,32 +99,32 @@ public SshResult runScript(String script) { tempPasswordFile = File.createTempFile("zstack", "tmp"); writeSecretFile(tempPasswordFile, privateKey); ssh = ln( - "ssh -q -i {0} -o UserKnownHostsFile=/dev/null -o PasswordAuthentication=no -o StrictHostKeyChecking=no -p {1} -T {2}@{3} << 'EOF'", + "ssh -q -i {0} -o UserKnownHostsFile=/dev/null -o PasswordAuthentication=no -o StrictHostKeyChecking=no -p {1} -T {2} << 'EOF'", "s=`mktemp`", "cat << 'EOT' > $s", - "{4}", + "{3}", "EOT", "bash $s", "ret=$?", "rm -f $s", "exit $ret", "EOF" - ).format(tempPasswordFile.getAbsolutePath(), port, username, hostname, script); + ).format(tempPasswordFile.getAbsolutePath(), port, formatSshTarget(username, hostname), script); } else { tempPasswordFile = File.createTempFile("zstack", "tmp"); writeSecretFile(tempPasswordFile, password); ssh = ln( - "sshpass -f{0} ssh -q -o UserKnownHostsFile=/dev/null -o PubkeyAuthentication=no -o StrictHostKeyChecking=no -p {1} -T {2}@{3} << 'EOF'", + "sshpass -f{0} ssh -q -o UserKnownHostsFile=/dev/null -o PubkeyAuthentication=no -o StrictHostKeyChecking=no -p {1} -T {2} << 'EOF'", "s=`mktemp`", "cat << 'EOT' > $s", - "{4}", + "{3}", "EOT", "bash $s", "ret=$?", "rm -f $s", "exit $ret", "EOF" - ).format(tempPasswordFile.getAbsolutePath(), port, username, hostname, script); + ).format(tempPasswordFile.getAbsolutePath(), port, formatSshTarget(username, hostname), script); } if (logger.isTraceEnabled()) { From 3dc8bcd013d7ea2aee3bd9824476afd4213d0fff Mon Sep 17 00:00:00 2001 From: "shixin.ruan" Date: Thu, 28 May 2026 01:50:21 +0900 Subject: [PATCH 34/53] [utils]: handle ipv6 in system tags VXLAN CIDR system tags can contain IPv6 values with double colon. Split tag fields only outside token braces so patterned tag matching and token extraction keep IPv6 CIDRs intact. Resolves: ZSTAC-85618 Change-Id: Ie74a3ac89e1d728953bcaab74146d25e7a7e2edc --- .../core/ManagementNetworkIpv6Case.groovy | 27 +++++++++++ .../main/java/org/zstack/utils/TagUtils.java | 45 ++++++++++++++++--- 2 files changed, 67 insertions(+), 5 deletions(-) diff --git a/test/src/test/groovy/org/zstack/test/integration/core/ManagementNetworkIpv6Case.groovy b/test/src/test/groovy/org/zstack/test/integration/core/ManagementNetworkIpv6Case.groovy index ed41c0e44e7..1e3886bf9e4 100644 --- a/test/src/test/groovy/org/zstack/test/integration/core/ManagementNetworkIpv6Case.groovy +++ b/test/src/test/groovy/org/zstack/test/integration/core/ManagementNetworkIpv6Case.groovy @@ -16,6 +16,7 @@ import org.zstack.kvm.KVMConsoleHypervisorBackend import org.zstack.kvm.KVMHost import org.zstack.kvm.KvmHostIpmiPowerExecutor import org.zstack.network.l2.vxlan.vxlanNetworkPool.VxlanPoolApiInterceptor +import org.zstack.network.l2.vxlan.vxlanNetworkPool.VxlanSystemTags import org.zstack.storage.ceph.MonUri import org.zstack.storage.ceph.backup.CephBackupStorageMetaDataMaker import org.zstack.storage.primary.nfs.NfsApiParamChecker @@ -44,6 +45,10 @@ class ManagementNetworkIpv6Case extends SubCase { private static final String NFS_IPV6_URL = "[${IPV6}]:${NFS_EXPORT_PATH}" private static final String CEPH_IPV6_MON_URL = "root:password@[${IPV6}]:22/?monPort=6789" private static final String INVALID_VTEP_IP = "not-a-vtep-ip" + private static final String VXLAN_POOL_UUID = "235f904603a2416d83810ff1dd5850b8" + private static final String CLUSTER_UUID = "e9acb8d6a4b04eea89f14e91918deed7" + private static final String VXLAN_IPV4_CIDR = "192.168.100.0/24" + private static final String VXLAN_IPV6_CIDR = "fd00:172:24:249::/64" private static final String HOST_EXTRA_IPS = "10.0.0.10,${IPV6_2}" private static final String IPV4_ADDRESS_COMMAND_OUTPUT = """\ 2: eth0 @@ -108,6 +113,7 @@ class ManagementNetworkIpv6Case extends SubCase { testCephIpv6MonUrlParsing() testCephMetadataAgentUrlUsesBracketedIpv6Host() testVxlanVtepIpv6Validation() + testVxlanSystemTagMatchesIpv6Cidr() testKvmExtraIpCidrSelection() testKvmIpmiAddressKeepsIpv6() testApplianceVmBootstrapParam() @@ -386,6 +392,27 @@ class ManagementNetworkIpv6Case extends SubCase { assert VxlanPoolApiInterceptor.normalizeVtepIp(" ${IPV6_FULL}\n") == IPV6 } + void testVxlanSystemTagMatchesIpv6Cidr() { + String ipv4Tag = VxlanSystemTags.VXLAN_POOL_CLUSTER_VTEP_CIDR.instantiateTag([ + (VxlanSystemTags.VXLAN_POOL_UUID_TOKEN): VXLAN_POOL_UUID, + (VxlanSystemTags.CLUSTER_UUID_TOKEN) : CLUSTER_UUID, + (VxlanSystemTags.VTEP_CIDR_TOKEN) : "{${VXLAN_IPV4_CIDR}}" + ]) + String ipv6Tag = VxlanSystemTags.VXLAN_POOL_CLUSTER_VTEP_CIDR.instantiateTag([ + (VxlanSystemTags.VXLAN_POOL_UUID_TOKEN): VXLAN_POOL_UUID, + (VxlanSystemTags.CLUSTER_UUID_TOKEN) : CLUSTER_UUID, + (VxlanSystemTags.VTEP_CIDR_TOKEN) : "{${VXLAN_IPV6_CIDR}}" + ]) + + assert VxlanSystemTags.VXLAN_POOL_CLUSTER_VTEP_CIDR.isMatch(ipv4Tag) + assert VxlanSystemTags.VXLAN_POOL_CLUSTER_VTEP_CIDR.isMatch(ipv6Tag) + + def tokens = VxlanSystemTags.VXLAN_POOL_CLUSTER_VTEP_CIDR.getTokensByTag(ipv6Tag) + assert tokens[VxlanSystemTags.VXLAN_POOL_UUID_TOKEN] == VXLAN_POOL_UUID + assert tokens[VxlanSystemTags.CLUSTER_UUID_TOKEN] == CLUSTER_UUID + assert tokens[VxlanSystemTags.VTEP_CIDR_TOKEN] == "{${VXLAN_IPV6_CIDR}}" + } + void testKvmExtraIpCidrSelection() { assert KVMHost.selectIpInCidr(HOST_EXTRA_IPS, "10.0.0.0/24") == "10.0.0.10" assert KVMHost.selectIpInCidr(HOST_EXTRA_IPS, "2001:db8::/64") == IPV6_2 diff --git a/utils/src/main/java/org/zstack/utils/TagUtils.java b/utils/src/main/java/org/zstack/utils/TagUtils.java index 823a7af6dc2..e2537872af2 100755 --- a/utils/src/main/java/org/zstack/utils/TagUtils.java +++ b/utils/src/main/java/org/zstack/utils/TagUtils.java @@ -8,12 +8,16 @@ /** */ public class TagUtils { + private static final String TAG_DELIMITER = "::"; + private static final char TOKEN_START = '{'; + private static final char TOKEN_END = '}'; + public static Map parse(String fmt, String tag) { List origins = new ArrayList(); - Collections.addAll(origins, tag.split("::")); + origins.addAll(splitTagFields(tag)); List t = new ArrayList(); - Collections.addAll(t, fmt.split("::")); + t.addAll(splitTagFields(fmt)); Map ret = new HashMap(); for (int i=0;i origins = new ArrayList(); - Collections.addAll(origins, tag.split("::")); + origins.addAll(splitTagFields(tag)); List t = new ArrayList(); - Collections.addAll(t, fmt.split("::")); + t.addAll(splitTagFields(fmt)); - if (fmt.indexOf("::") == -1) { + if (fmt.indexOf(TAG_DELIMITER) == -1) { return fmt.equals(tag); } @@ -66,6 +70,37 @@ public static boolean isMatch(String fmt, String tag) { return true; } + private static List splitTagFields(String tag) { + List fields = new ArrayList<>(); + StringBuilder field = new StringBuilder(); + int braceDepth = 0; + + for (int i = 0; i < tag.length(); i++) { + char current = tag.charAt(i); + if (current == TOKEN_START) { + braceDepth++; + } else if (current == TOKEN_END && braceDepth > 0) { + braceDepth--; + } + + if (braceDepth == 0 && tag.startsWith(TAG_DELIMITER, i)) { + fields.add(field.toString()); + field.setLength(0); + i += TAG_DELIMITER.length() - 1; + continue; + } + + field.append(current); + } + + fields.add(field.toString()); + while (!fields.isEmpty() && fields.get(fields.size() - 1).isEmpty()) { + fields.remove(fields.size() - 1); + } + + return fields; + } + public static Map parseIfMatch(String fmt, String tag) { if (!isMatch(fmt, tag)) { return null; From 81ed0a121602a5e93730fdfc4dd4a18ac4fe6982 Mon Sep 17 00:00:00 2001 From: "shixin.ruan" Date: Thu, 28 May 2026 12:01:07 +0900 Subject: [PATCH 35/53] [core]: remove ipv6 preference switch Remove the customer-facing management.server.prefer.ipv6 switch.\n\nUse explicit management.server.ip first. When it is not configured, keep IPv4 as the default and fall back to IPv6 only if no IPv4 is available.\n\nWaterfall CR: ZSTAC-79206 CR-001 Change-Id: I14e3e6b3fdad2e4e109d4f9c9f3f344356866762 --- .../org/zstack/core/CoreGlobalProperty.java | 3 - .../core/ManagementServerGlobalConfig.java | 16 ----- .../main/java/org/zstack/core/Platform.java | 22 +------ .../core/ManagementNetworkIpv6Case.groovy | 64 ++----------------- 4 files changed, 9 insertions(+), 96 deletions(-) delete mode 100644 core/src/main/java/org/zstack/core/ManagementServerGlobalConfig.java diff --git a/core/src/main/java/org/zstack/core/CoreGlobalProperty.java b/core/src/main/java/org/zstack/core/CoreGlobalProperty.java index d281dbb7016..828b06b0bf2 100755 --- a/core/src/main/java/org/zstack/core/CoreGlobalProperty.java +++ b/core/src/main/java/org/zstack/core/CoreGlobalProperty.java @@ -79,9 +79,6 @@ public class CoreGlobalProperty { public static List CHRONY_SERVERS; @GlobalProperty(name="management.server.vip") public static String MN_VIP; - @GlobalProperty(name = "management.server.prefer.ipv6", defaultValue = "false") - @AvailableValues(value = {"true", "false"}) - public static boolean MANAGEMENT_SERVER_PREFER_IPV6; @GlobalProperty(name = "simulatorsOn", defaultValue = "false") public static boolean SIMULATORS_ON; @GlobalProperty(name = "startMode", defaultValue = "") diff --git a/core/src/main/java/org/zstack/core/ManagementServerGlobalConfig.java b/core/src/main/java/org/zstack/core/ManagementServerGlobalConfig.java deleted file mode 100644 index d72c766497f..00000000000 --- a/core/src/main/java/org/zstack/core/ManagementServerGlobalConfig.java +++ /dev/null @@ -1,16 +0,0 @@ -package org.zstack.core; - -import org.zstack.core.config.GlobalConfig; -import org.zstack.core.config.GlobalConfigDef; -import org.zstack.core.config.GlobalConfigDefinition; -import org.zstack.core.config.GlobalConfigValidation; - -@GlobalConfigDefinition -public class ManagementServerGlobalConfig { - public static final String CATEGORY = "management.server"; - - @GlobalConfigDef(defaultValue = "false", type = Boolean.class, - description = "Prefer IPv6 when selecting the management server IP on dual-stack hosts") - @GlobalConfigValidation(validValues = {"true", "false"}) - public static GlobalConfig PREFER_IPV6 = new GlobalConfig(CATEGORY, "prefer.ipv6"); -} diff --git a/core/src/main/java/org/zstack/core/Platform.java b/core/src/main/java/org/zstack/core/Platform.java index cb20cc3bb4a..cc2020b22f2 100755 --- a/core/src/main/java/org/zstack/core/Platform.java +++ b/core/src/main/java/org/zstack/core/Platform.java @@ -83,7 +83,6 @@ public class Platform { private static MessageSource messageSource; private static String encryptionKey = EncryptRSA.generateKeyString("ZStack open source"); private static final String MANAGEMENT_SERVER_IP_PROPERTY = "management.server.ip"; - private static final String MANAGEMENT_SERVER_PREFER_IPV6_PROPERTY = "management.server.prefer.ipv6"; private static final String ZSTACK_MANAGEMENT_SERVER_IP_ENV = "ZSTACK_MANAGEMENT_SERVER_IP"; private static final String IPV4_ADDRESS_COMMAND = "ip -4 add"; private static final String IPV6_ADDRESS_COMMAND = "ip -6 addr"; @@ -994,7 +993,7 @@ private static String getManagementServerIpInternal() { for (NetworkInterface iface : Collections.list(nets)) { String name = iface.getName(); if (defaultLine.contains(name)) { - ip = selectManagementServerIp(Collections.list(iface.getInetAddresses()), isManagementServerPreferIpv6()); + ip = selectManagementServerIp(Collections.list(iface.getInetAddresses())); } } } catch (SocketException e) { @@ -1070,7 +1069,7 @@ public static List getManagementServerIps() { return new ArrayList<>(ips); } - public static String selectManagementServerIp(Collection addresses, boolean preferIpv6) { + public static String selectManagementServerIp(Collection addresses) { String ipv4 = null; String ipv6 = null; @@ -1087,26 +1086,9 @@ public static String selectManagementServerIp(Collection addresses, } } - if (preferIpv6 && ipv6 != null) { - return ipv6; - } - return ipv4 != null ? ipv4 : ipv6; } - public static boolean isManagementServerPreferIpv6() { - String propertyValue = System.getProperty(MANAGEMENT_SERVER_PREFER_IPV6_PROPERTY); - if (propertyValue != null) { - return Boolean.parseBoolean(propertyValue); - } - - try { - return ManagementServerGlobalConfig.PREFER_IPV6.value(Boolean.class); - } catch (RuntimeException e) { - return CoreGlobalProperty.MANAGEMENT_SERVER_PREFER_IPV6; - } - } - public static String formatJGroupsInitialHosts(String nodeIp, String peerIp, int port) { return String.format(JGROUPS_INITIAL_HOST_FORMAT, IPv6NetworkUtils.formatHostForUrl(nodeIp), port, diff --git a/test/src/test/groovy/org/zstack/test/integration/core/ManagementNetworkIpv6Case.groovy b/test/src/test/groovy/org/zstack/test/integration/core/ManagementNetworkIpv6Case.groovy index 1e3886bf9e4..7b0f90821a1 100644 --- a/test/src/test/groovy/org/zstack/test/integration/core/ManagementNetworkIpv6Case.groovy +++ b/test/src/test/groovy/org/zstack/test/integration/core/ManagementNetworkIpv6Case.groovy @@ -4,8 +4,6 @@ import org.zstack.appliancevm.ApplianceVmConstant import org.zstack.appliancevm.ApplianceVmFacadeImpl import org.zstack.core.ansible.CallBackNetworkChecker import org.zstack.core.ansible.AnsibleRunner -import org.zstack.core.CoreGlobalProperty -import org.zstack.core.ManagementServerGlobalConfig import org.zstack.core.Platform import org.zstack.core.agent.AgentManagerImpl import org.zstack.core.cloudbus.CloudBusImpl3 @@ -81,10 +79,6 @@ class ManagementNetworkIpv6Case extends SubCase { @Override @Test void test() { - testPreferIpv6DefaultFalse() - testPreferIpv6GlobalConfigDefinition() - testPreferIpv6GlobalConfigValue() - testPreferIpv6SystemProperty() testSelectManagementServerIpDualStackPolicy() testSelectManagementServerIpSkipsLoopbackAndLinkLocal() testSelectApplianceVmManagementNodeIpByCidr() @@ -119,58 +113,14 @@ class ManagementNetworkIpv6Case extends SubCase { testApplianceVmBootstrapParam() } - void testPreferIpv6DefaultFalse() { - assert !CoreGlobalProperty.MANAGEMENT_SERVER_PREFER_IPV6 - } - - void testPreferIpv6GlobalConfigDefinition() { - assert ManagementServerGlobalConfig.PREFER_IPV6.category == "management.server" - assert ManagementServerGlobalConfig.PREFER_IPV6.name == "prefer.ipv6" - } - - void testPreferIpv6GlobalConfigValue() { - String oldPropertyValue = System.getProperty("management.server.prefer.ipv6") - String oldGlobalConfigValue = ManagementServerGlobalConfig.PREFER_IPV6.@value - try { - System.clearProperty("management.server.prefer.ipv6") - ManagementServerGlobalConfig.PREFER_IPV6.@value = "true" - assert Platform.isManagementServerPreferIpv6() - ManagementServerGlobalConfig.PREFER_IPV6.@value = "false" - assert !Platform.isManagementServerPreferIpv6() - } finally { - if (oldPropertyValue == null) { - System.clearProperty("management.server.prefer.ipv6") - } else { - System.setProperty("management.server.prefer.ipv6", oldPropertyValue) - } - ManagementServerGlobalConfig.PREFER_IPV6.@value = oldGlobalConfigValue - } - } - - void testPreferIpv6SystemProperty() { - String oldValue = System.getProperty("management.server.prefer.ipv6") - try { - System.setProperty("management.server.prefer.ipv6", "true") - assert Platform.isManagementServerPreferIpv6() - System.setProperty("management.server.prefer.ipv6", "false") - assert !Platform.isManagementServerPreferIpv6() - } finally { - if (oldValue == null) { - System.clearProperty("management.server.prefer.ipv6") - } else { - System.setProperty("management.server.prefer.ipv6", oldValue) - } - } - } - void testSelectManagementServerIpDualStackPolicy() { def ipv4 = InetAddress.getByName(IPV4) def ipv6 = InetAddress.getByName(IPV6) - assert Platform.selectManagementServerIp([ipv6, ipv4], false) == IPV4 - assert Platform.selectManagementServerIp([ipv4, ipv6], true) == IPV6 - assert Platform.selectManagementServerIp([ipv6], false) == IPV6 - assert Platform.selectManagementServerIp([ipv4], true) == IPV4 + assert Platform.selectManagementServerIp([ipv6, ipv4]) == IPV4 + assert Platform.selectManagementServerIp([ipv4, ipv6]) == IPV4 + assert Platform.selectManagementServerIp([ipv6]) == IPV6 + assert Platform.selectManagementServerIp([ipv4]) == IPV4 } void testSelectManagementServerIpSkipsLoopbackAndLinkLocal() { @@ -180,9 +130,9 @@ class ManagementNetworkIpv6Case extends SubCase { def loopbackIpv6 = InetAddress.getByName(LOOPBACK_IPV6) def linkLocalIpv6 = InetAddress.getByName(LINK_LOCAL_IPV6) - assert Platform.selectManagementServerIp([loopbackIpv4, ipv4], false) == IPV4 - assert Platform.selectManagementServerIp([loopbackIpv6, linkLocalIpv6, ipv6], true) == IPV6 - assert Platform.selectManagementServerIp([loopbackIpv4, loopbackIpv6, linkLocalIpv6], true) == null + assert Platform.selectManagementServerIp([loopbackIpv4, ipv4]) == IPV4 + assert Platform.selectManagementServerIp([loopbackIpv6, linkLocalIpv6, ipv6]) == IPV6 + assert Platform.selectManagementServerIp([loopbackIpv4, loopbackIpv6, linkLocalIpv6]) == null } void testSelectApplianceVmManagementNodeIpByCidr() { From 4ca12c487af0b43b286027837c2653e23068ce2a Mon Sep 17 00:00:00 2001 From: "shixin.ruan" Date: Thu, 28 May 2026 13:48:06 +0900 Subject: [PATCH 36/53] [kvm]: fix ipv6 kvm agent urls Build KVM agent HTTP URLs through the IPv6-safe host formatter so live migration cleanup and migrate calls bracket IPv6 host addresses. Resolves: ZSTAC-85636 Change-Id: I0c9fa1eecf7d6eef778cbb4568123d7bd3a836ec --- .../src/main/java/org/zstack/kvm/KVMHost.java | 29 ++++++++----------- .../core/ManagementNetworkIpv6Case.groovy | 12 ++++++++ 2 files changed, 24 insertions(+), 17 deletions(-) diff --git a/plugin/kvm/src/main/java/org/zstack/kvm/KVMHost.java b/plugin/kvm/src/main/java/org/zstack/kvm/KVMHost.java index eae09b34930..36e53287727 100755 --- a/plugin/kvm/src/main/java/org/zstack/kvm/KVMHost.java +++ b/plugin/kvm/src/main/java/org/zstack/kvm/KVMHost.java @@ -2726,10 +2726,10 @@ public void done() { ); } - private String buildUrl(String path) { + public static String buildAgentUrl(String host, String path) { UriComponentsBuilder ub = UriComponentsBuilder.newInstance(); ub.scheme(KVMGlobalProperty.AGENT_URL_SCHEME); - ub.host(KVMHostUtils.formatHostForUrl(self.getManagementIp())); + ub.host(KVMHostUtils.formatHostForUrl(host)); ub.port(KVMGlobalProperty.AGENT_PORT); if (!"".equals(KVMGlobalProperty.AGENT_URL_ROOT_PATH)) { ub.path(KVMGlobalProperty.AGENT_URL_ROOT_PATH); @@ -2738,6 +2738,10 @@ private String buildUrl(String path) { return ub.build().toUriString(); } + private String buildUrl(String path) { + return buildAgentUrl(self.getManagementIp(), path); + } + private void executeAsyncHttpCall(final KVMHostAsyncHttpCallMsg msg, final NoErrorCompletion completion) { if (!msg.isNoStatusCheck()) { checkStatus(); @@ -3154,10 +3158,7 @@ public void run(final FlowTrigger trigger, Map data) { CleanVmFirmwareFlashCmd cmd = new CleanVmFirmwareFlashCmd(); cmd.vmUuid = vmUuid; - UriComponentsBuilder ub = UriComponentsBuilder.fromHttpUrl(baseUrl); - ub.host(dstHostMnIp); - ub.path(KVMConstant.CLEAN_FIRMWARE_FLASH); - String url = ub.build().toString(); + String url = buildAgentUrl(dstHostMnIp, KVMConstant.CLEAN_FIRMWARE_FLASH); new Http<>(url, cmd, AgentResponse.class).call(dstHostUuid, new ReturnValueCompletion(trigger) { @Override public void success(AgentResponse ret) { @@ -3227,9 +3228,9 @@ protected void scripts() { cmd.setDisks(diskMigrationMap); } - UriComponentsBuilder ub = UriComponentsBuilder.fromHttpUrl(migrateVmPath); - ub.host(migrateFromDestination ? dstHostMnIp : srcHostMnIp); - String migrateUrl = ub.build().toString(); + String migrateUrl = buildAgentUrl( + migrateFromDestination ? dstHostMnIp : srcHostMnIp, + KVMConstant.KVM_MIGRATE_VM_PATH); new Http<>(migrateUrl, cmd, MigrateVmResponse.class).call(migrateFromDestination ? dstHostUuid : srcHostUuid, new ReturnValueCompletion(trigger) { @Override public void success(MigrateVmResponse ret) { @@ -3268,10 +3269,7 @@ public void run(final FlowTrigger trigger, Map data) { cmd.vmUuid = vmUuid; cmd.hostManagementIp = dstHostMnIp; - UriComponentsBuilder ub = UriComponentsBuilder.fromHttpUrl(baseUrl); - ub.host(dstHostMnIp); - ub.path(KVMConstant.KVM_HARDEN_CONSOLE_PATH); - String url = ub.build().toString(); + String url = buildAgentUrl(dstHostMnIp, KVMConstant.KVM_HARDEN_CONSOLE_PATH); new Http<>(url, cmd, AgentResponse.class).call(dstHostUuid, new ReturnValueCompletion(trigger) { @Override public void success(AgentResponse ret) { @@ -3304,10 +3302,7 @@ public void run(final FlowTrigger trigger, Map data) { cmd.vmUuid = vmUuid; cmd.hostManagementIp = srcHostMnIp; - UriComponentsBuilder ub = UriComponentsBuilder.fromHttpUrl(baseUrl); - ub.host(srcHostMnIp); - ub.path(KVMConstant.KVM_DELETE_CONSOLE_FIREWALL_PATH); - String url = ub.build().toString(); + String url = buildAgentUrl(srcHostMnIp, KVMConstant.KVM_DELETE_CONSOLE_FIREWALL_PATH); new Http<>(url, cmd, AgentResponse.class).call(new ReturnValueCompletion(trigger) { @Override public void success(AgentResponse ret) { diff --git a/test/src/test/groovy/org/zstack/test/integration/core/ManagementNetworkIpv6Case.groovy b/test/src/test/groovy/org/zstack/test/integration/core/ManagementNetworkIpv6Case.groovy index 7b0f90821a1..cb2d1ce2831 100644 --- a/test/src/test/groovy/org/zstack/test/integration/core/ManagementNetworkIpv6Case.groovy +++ b/test/src/test/groovy/org/zstack/test/integration/core/ManagementNetworkIpv6Case.groovy @@ -11,7 +11,9 @@ import org.zstack.core.rest.RESTFacadeImpl import org.zstack.console.ConsoleProxyBase import org.zstack.header.rest.RESTConstant import org.zstack.kvm.KVMConsoleHypervisorBackend +import org.zstack.kvm.KVMConstant import org.zstack.kvm.KVMHost +import org.zstack.kvm.KVMGlobalProperty import org.zstack.kvm.KvmHostIpmiPowerExecutor import org.zstack.network.l2.vxlan.vxlanNetworkPool.VxlanPoolApiInterceptor import org.zstack.network.l2.vxlan.vxlanNetworkPool.VxlanSystemTags @@ -88,6 +90,7 @@ class ManagementNetworkIpv6Case extends SubCase { testConsoleVncUriIpv6() testConsoleProxyListenHostByProxyIpVersion() testCoreManagementUrlsIpv6() + testKvmAgentUrlsIpv6() testRestFacadeIpv6Urls() testSshTargetUsesRawIpv6Host() testScpTargetUsesBracketedIpv6Host() @@ -184,6 +187,15 @@ class ManagementNetworkIpv6Case extends SubCase { assert AnsibleRunner.buildPipUrl(IPV6, REST_PORT) == "http://[2001:db8::1]:8080/zstack/static/pypi/simple" } + void testKvmAgentUrlsIpv6() { + assert KVMHost.buildAgentUrl(IPV6, KVMConstant.KVM_MIGRATE_VM_PATH) == + "http://[2001:db8::1]:${KVMGlobalProperty.AGENT_PORT}/vm/migrate" + assert KVMHost.buildAgentUrl(IPV6, KVMConstant.CLEAN_FIRMWARE_FLASH) == + "http://[2001:db8::1]:${KVMGlobalProperty.AGENT_PORT}/clean/firmware/flash" + assert KVMHost.buildAgentUrl(IPV4, KVMConstant.KVM_MIGRATE_VM_PATH) == + "http://192.168.1.10:${KVMGlobalProperty.AGENT_PORT}/vm/migrate" + } + void testRestFacadeIpv6Urls() { assert RESTFacadeImpl.buildBaseUrl(IPV6, REST_PORT, null) == "http://[2001:db8::1]:8080" assert RESTFacadeImpl.buildBaseUrl(IPV6, REST_PORT, "zstack") == "http://[2001:db8::1]:8080/zstack" From a31cf9cf674bad865e0b8f8c8034f18c4df03400 Mon Sep 17 00:00:00 2001 From: "shixin.ruan" Date: Thu, 28 May 2026 14:42:46 +0900 Subject: [PATCH 37/53] [mgt-ipv6]: fix ipv6 migrate cidr Allow patterned system tag values to contain IPv6 token text. Use dual-stack CIDR matching for migration network selection. Add focused management IPv6 regression assertions. Resolves: ZSTAC-85638 Change-Id: I32f3d1111aa3358fd26da6a3afb23111154f9058 --- .../ceph/primary/CephPrimaryStorageBase.java | 2 +- .../core/ManagementNetworkIpv6Case.groovy | 17 ++++++ .../main/java/org/zstack/utils/TagUtils.java | 58 ++++++++++++++++--- .../zstack/utils/network/NetworkUtils.java | 16 ++++- 4 files changed, 83 insertions(+), 10 deletions(-) diff --git a/plugin/ceph/src/main/java/org/zstack/storage/ceph/primary/CephPrimaryStorageBase.java b/plugin/ceph/src/main/java/org/zstack/storage/ceph/primary/CephPrimaryStorageBase.java index 285e970de68..c5da3bc36b4 100755 --- a/plugin/ceph/src/main/java/org/zstack/storage/ceph/primary/CephPrimaryStorageBase.java +++ b/plugin/ceph/src/main/java/org/zstack/storage/ceph/primary/CephPrimaryStorageBase.java @@ -5869,7 +5869,7 @@ public MonMigrateIpInfo(String psUuid) { final String extraIps = CephMonSystemTags.EXTRA_IPS .getTokenByResourceUuid(mon.getUuid(), CephMonSystemTags.EXTRA_IPS_TOKEN); Optional.ofNullable(extraIps).ifPresent(it -> ips.addAll(Arrays.asList(it.split(",")))); - List cidrIps = NetworkUtils.filterIpv4sInCidr(ips, migrateCidr); + List cidrIps = NetworkUtils.filterIpsInCidr(ips, migrateCidr); if (!cidrIps.isEmpty()) { monMigrateIpMap.put(mon.getUuid(), cidrIps.get(0)); } diff --git a/test/src/test/groovy/org/zstack/test/integration/core/ManagementNetworkIpv6Case.groovy b/test/src/test/groovy/org/zstack/test/integration/core/ManagementNetworkIpv6Case.groovy index cb2d1ce2831..463a4599e84 100644 --- a/test/src/test/groovy/org/zstack/test/integration/core/ManagementNetworkIpv6Case.groovy +++ b/test/src/test/groovy/org/zstack/test/integration/core/ManagementNetworkIpv6Case.groovy @@ -21,6 +21,7 @@ import org.zstack.storage.ceph.MonUri import org.zstack.storage.ceph.backup.CephBackupStorageMetaDataMaker import org.zstack.storage.primary.nfs.NfsApiParamChecker import org.zstack.testlib.SubCase +import org.zstack.utils.TagUtils import org.zstack.utils.URLBuilder import org.zstack.utils.ssh.SshShell import org.zstack.utils.network.IPv6Constants @@ -111,6 +112,7 @@ class ManagementNetworkIpv6Case extends SubCase { testCephMetadataAgentUrlUsesBracketedIpv6Host() testVxlanVtepIpv6Validation() testVxlanSystemTagMatchesIpv6Cidr() + testPatternedSystemTagParsesIpv6Token() testKvmExtraIpCidrSelection() testKvmIpmiAddressKeepsIpv6() testApplianceVmBootstrapParam() @@ -263,6 +265,7 @@ class ManagementNetworkIpv6Case extends SubCase { void testIpv6NetworkCidr() { assert NetworkUtils.getNetworkAddressFromCidr("2001:db8::1/64") == "2001:db8::/64" + assert NetworkUtils.fmtCidr("2001:db8::1/64") == "2001:db8::/64" } void testIpInCidrDualStack() { @@ -270,6 +273,8 @@ class ManagementNetworkIpv6Case extends SubCase { assert NetworkUtils.isIpInCidr(IPV6, "2001:db8::/64") assert !NetworkUtils.isIpInCidr(IPV4, "2001:db8::/64") assert !NetworkUtils.isIpInCidr(IPV6, "192.168.1.0/24") + assert NetworkUtils.filterIpsInCidr([IPV4, IPV6], "192.168.1.0/24") == [IPV4] + assert NetworkUtils.filterIpsInCidr([IPV4, IPV6], "2001:db8::/64") == [IPV6] } void testManagementCidrCommandOutputParsing() { @@ -375,6 +380,18 @@ class ManagementNetworkIpv6Case extends SubCase { assert tokens[VxlanSystemTags.VTEP_CIDR_TOKEN] == "{${VXLAN_IPV6_CIDR}}" } + void testPatternedSystemTagParsesIpv6Token() { + String extraIpsFormat = "extraips::{extraips}" + String extraIpsTag = "extraips::10.0.0.10,${IPV6_2}" + assert TagUtils.isMatch(extraIpsFormat, extraIpsTag) + assert TagUtils.parseIfMatch(extraIpsFormat, extraIpsTag)["extraips"] == "10.0.0.10,${IPV6_2}" + + String migrateCidrFormat = "cluster::migrate::network::cidr::{migrateCidr}" + String migrateCidrTag = "cluster::migrate::network::cidr::${VXLAN_IPV6_CIDR}" + assert TagUtils.isMatch(migrateCidrFormat, migrateCidrTag) + assert TagUtils.parseIfMatch(migrateCidrFormat, migrateCidrTag)["migrateCidr"] == VXLAN_IPV6_CIDR + } + void testKvmExtraIpCidrSelection() { assert KVMHost.selectIpInCidr(HOST_EXTRA_IPS, "10.0.0.0/24") == "10.0.0.10" assert KVMHost.selectIpInCidr(HOST_EXTRA_IPS, "2001:db8::/64") == IPV6_2 diff --git a/utils/src/main/java/org/zstack/utils/TagUtils.java b/utils/src/main/java/org/zstack/utils/TagUtils.java index e2537872af2..5058615062d 100755 --- a/utils/src/main/java/org/zstack/utils/TagUtils.java +++ b/utils/src/main/java/org/zstack/utils/TagUtils.java @@ -13,13 +13,15 @@ public class TagUtils { private static final char TOKEN_END = '}'; public static Map parse(String fmt, String tag) { - List origins = new ArrayList(); - origins.addAll(splitTagFields(tag)); - List t = new ArrayList(); t.addAll(splitTagFields(fmt)); + List origins = splitTagFieldsByFormat(t, tag); Map ret = new HashMap(); + if (origins == null) { + return ret; + } + for (int i=0;i origins = new ArrayList(); - origins.addAll(splitTagFields(tag)); - List t = new ArrayList(); t.addAll(splitTagFields(fmt)); @@ -51,7 +50,8 @@ public static boolean isMatch(String fmt, String tag) { return fmt.equals(tag); } - if (origins.size() != t.size()) { + List origins = splitTagFieldsByFormat(t, tag); + if (origins == null || origins.size() != t.size()) { return false; } @@ -70,6 +70,50 @@ public static boolean isMatch(String fmt, String tag) { return true; } + private static List splitTagFieldsByFormat(List fmtFields, String tag) { + List fields = new ArrayList<>(); + int offset = 0; + + for (int i = 0; i < fmtFields.size(); i++) { + String fmtField = fmtFields.get(i); + boolean lastField = i == fmtFields.size() - 1; + if (isTokenField(fmtField)) { + if (lastField) { + fields.add(tag.substring(offset)); + offset = tag.length(); + continue; + } + + int end = tag.indexOf(TAG_DELIMITER, offset); + if (end < 0) { + return null; + } + fields.add(tag.substring(offset, end)); + offset = end + TAG_DELIMITER.length(); + continue; + } + + if (!tag.startsWith(fmtField, offset)) { + return null; + } + + fields.add(fmtField); + offset += fmtField.length(); + if (!lastField) { + if (!tag.startsWith(TAG_DELIMITER, offset)) { + return null; + } + offset += TAG_DELIMITER.length(); + } + } + + return offset == tag.length() ? fields : null; + } + + private static boolean isTokenField(String field) { + return field.startsWith(String.valueOf(TOKEN_START)) && field.endsWith(String.valueOf(TOKEN_END)); + } + private static List splitTagFields(String tag) { List fields = new ArrayList<>(); StringBuilder field = new StringBuilder(); diff --git a/utils/src/main/java/org/zstack/utils/network/NetworkUtils.java b/utils/src/main/java/org/zstack/utils/network/NetworkUtils.java index 0bc3daef0c2..4cb6be81196 100755 --- a/utils/src/main/java/org/zstack/utils/network/NetworkUtils.java +++ b/utils/src/main/java/org/zstack/utils/network/NetworkUtils.java @@ -589,6 +589,19 @@ public static List filterIpv4sInCidr(List ipv4s, String cidr){ return results; } + public static List filterIpsInCidr(List ips, String cidr){ + DebugUtils.Assert(isCidr(cidr), String.format("%s is not a cidr", cidr)); + List results = new ArrayList<>(); + + for (String ip : ips) { + validateIp(ip); + if (isIpInCidr(ip, cidr)) { + results.add(ip); + } + } + return results; + } + public static boolean isIpRoutedByDefaultGateway(String ip) { ShellResult res = ShellUtils.runAndReturn(String.format("ip route get %s | grep -q \"via $(ip route | awk '/default/ {print $3}')\"", ip)); return res.isReturnCode(0); @@ -628,9 +641,8 @@ public static List getIpRangeFromIps(List ips){ } public static String fmtCidr(final String origin) { - // format "*.*.1.1/16" to "*.*.0.0/16" DebugUtils.Assert(isCidr(origin), String.format("%s is not a cidr", origin)); - return new SubnetUtils(origin).getInfo().getNetworkAddress() + "/" + origin.split("/")[1]; + return getNetworkAddressFromCidr(origin); } public static List getCidrsFromIpRange(String startIp, String endIp) { From 7d60eedd7ad2e3c35c43e14e3eed9e9db0d3336a Mon Sep 17 00:00:00 2001 From: "shixin.ruan" Date: Wed, 27 May 2026 16:42:10 +0900 Subject: [PATCH 38/53] [vrouter]: select callback IP for VR Use a callback URL in the same IP family as the virtual router management NIC. This keeps IPv4-only vrouter agents reachable when MN is dual-stack and its default management IP is IPv6. Resolves: ZSTAC-85527 Change-Id: Iaa630fc59d30c2675db7bbe47cb1b7c8d58bb023 --- .../org/zstack/core/rest/RESTFacadeImpl.java | 5 ++++ .../org/zstack/header/rest/RESTFacade.java | 2 ++ .../service/virtualrouter/VirtualRouter.java | 6 ++-- .../virtualrouter/VirtualRouterManager.java | 3 ++ .../VirtualRouterManagerImpl.java | 28 +++++++++++++++++++ .../VirtualRouterDeployAgentFlow.java | 4 +-- .../virtualrouter/vyos/VyosConnectFlow.java | 2 +- 7 files changed, 45 insertions(+), 5 deletions(-) diff --git a/core/src/main/java/org/zstack/core/rest/RESTFacadeImpl.java b/core/src/main/java/org/zstack/core/rest/RESTFacadeImpl.java index 41f1354ceb1..e4ae416ff42 100755 --- a/core/src/main/java/org/zstack/core/rest/RESTFacadeImpl.java +++ b/core/src/main/java/org/zstack/core/rest/RESTFacadeImpl.java @@ -986,6 +986,11 @@ public String getCallbackUrl() { return callbackUrl; } + @Override + public String buildCallbackUrl(String hostName) { + return buildCallbackUrl(hostName, port, path); + } + @Override public String getHostName() { return callbackHostName; diff --git a/header/src/main/java/org/zstack/header/rest/RESTFacade.java b/header/src/main/java/org/zstack/header/rest/RESTFacade.java index e9d3120f9d6..31f4ac15f4e 100755 --- a/header/src/main/java/org/zstack/header/rest/RESTFacade.java +++ b/header/src/main/java/org/zstack/header/rest/RESTFacade.java @@ -88,6 +88,8 @@ public interface RESTFacade { String getCallbackUrl(); + String buildCallbackUrl(String hostName); + String getHostName(); int getPort(); 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..e896b0f664a 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 @@ -187,7 +187,8 @@ protected void handleLocalMessage(Message msg) { void doPing(String vrUuid, ReturnValueCompletion completion) { PingCmd cmd = new PingCmd(); cmd.setUuid(vrUuid); - restf.asyncJsonPost(buildUrl(vr.getManagementNic().getIp(), VirtualRouterConstant.VR_PING), cmd, new JsonAsyncRESTCallback(completion) { + restf.asyncJsonPost(buildUrl(vr.getManagementNic().getIp(), VirtualRouterConstant.VR_PING), + cmd, vrMgr.buildAgentCallbackUrlHeaders(vr.getManagementNic().getIp()), new JsonAsyncRESTCallback(completion) { @Override public void fail(ErrorCode err) { completion.fail(err); @@ -685,7 +686,8 @@ public void run(final SyncTaskChain chain) { self.getUuid(), msg.getPath())); } - restf.asyncJsonPost(buildUrl(vr.getManagementNic().getIp(), msg.getPath()), msg.getCommand(), new JsonAsyncRESTCallback(msg, chain) { + restf.asyncJsonPost(buildUrl(vr.getManagementNic().getIp(), msg.getPath()), + msg.getCommand(), vrMgr.buildAgentCallbackUrlHeaders(vr.getManagementNic().getIp()), new JsonAsyncRESTCallback(msg, chain) { @Override public void fail(ErrorCode err) { reply.setError(err); diff --git a/plugin/virtualRouterProvider/src/main/java/org/zstack/network/service/virtualrouter/VirtualRouterManager.java b/plugin/virtualRouterProvider/src/main/java/org/zstack/network/service/virtualrouter/VirtualRouterManager.java index a7711086975..17a790f1020 100755 --- a/plugin/virtualRouterProvider/src/main/java/org/zstack/network/service/virtualrouter/VirtualRouterManager.java +++ b/plugin/virtualRouterProvider/src/main/java/org/zstack/network/service/virtualrouter/VirtualRouterManager.java @@ -11,6 +11,7 @@ import org.zstack.header.vm.VmNicInventory; import java.util.List; +import java.util.Map; public interface VirtualRouterManager { @@ -18,6 +19,8 @@ public interface VirtualRouterManager { String buildUrl(String mgmtNicIp, String subPath); + Map buildAgentCallbackUrlHeaders(String mgmtNicIp); + List selectL3NetworksNeedingSpecificNetworkService(List candidate, NetworkServiceType nsType); List selectGuestL3NetworksNeedingSpecificNetworkService(List candidate, NetworkServiceType nsType, String publicUuid); 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 a876ee24a34..3e435674ae7 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 @@ -50,6 +50,8 @@ import org.zstack.header.query.ExpandedQueryAliasStruct; import org.zstack.header.query.ExpandedQueryStruct; import org.zstack.header.query.QueryBelongFilter; +import org.zstack.header.rest.RESTConstant; +import org.zstack.header.rest.RESTFacade; import org.zstack.header.tag.*; import org.zstack.header.vm.*; import org.zstack.identity.Account; @@ -117,6 +119,9 @@ public class VirtualRouterManagerImpl extends AbstractService implements Virtual private final Map hypervisorBackends = new HashMap(); private final Map vrParallelismDegrees = new ConcurrentHashMap(); + @Autowired + private RESTFacade restf; + private List virtualRouterPostCreateFlows; private List virtualRouterPostStartFlows; private List virtualRouterPostRebootFlows; @@ -967,6 +972,29 @@ public String buildUrl(String mgmtNicIp, String subPath) { return ub.build().toUriString(); } + @Override + public Map buildAgentCallbackUrlHeaders(String mgmtNicIp) { + return Collections.singletonMap(RESTConstant.CALLBACK_URL, restf.buildCallbackUrl(selectManagementIpForAgent(mgmtNicIp))); + } + + private String selectManagementIpForAgent(String agentIp) { + if (IPv6NetworkUtils.isIpv6Address(agentIp)) { + return Platform.getManagementServerIps().stream() + .filter(IPv6NetworkUtils::isIpv6Address) + .findFirst() + .orElse(Platform.getManagementServerIp()); + } + + if (NetworkUtils.isIpv4Address(agentIp)) { + return Platform.getManagementServerIps().stream() + .filter(NetworkUtils::isIpv4Address) + .findFirst() + .orElse(Platform.getManagementServerIp()); + } + + return Platform.getManagementServerIp(); + } + private void buildWorkFlowBuilder() { postCreateFlowsBuilder = FlowChainBuilder.newBuilder().setFlowClassNames(virtualRouterPostCreateFlows).construct(); postStartFlowsBuilder = FlowChainBuilder.newBuilder().setFlowClassNames(virtualRouterPostStartFlows).construct(); diff --git a/plugin/virtualRouterProvider/src/main/java/org/zstack/network/service/virtualrouter/lifecycle/VirtualRouterDeployAgentFlow.java b/plugin/virtualRouterProvider/src/main/java/org/zstack/network/service/virtualrouter/lifecycle/VirtualRouterDeployAgentFlow.java index 903f53d18d7..0b6e0617a14 100755 --- a/plugin/virtualRouterProvider/src/main/java/org/zstack/network/service/virtualrouter/lifecycle/VirtualRouterDeployAgentFlow.java +++ b/plugin/virtualRouterProvider/src/main/java/org/zstack/network/service/virtualrouter/lifecycle/VirtualRouterDeployAgentFlow.java @@ -110,7 +110,7 @@ public void run(final FlowTrigger trigger, Map data) { cmd.setUuid(vr.getUuid()); cmd.setRestartDnsmasqAfterNumberOfSIGUSER1(VirtualRouterGlobalConfig.RESTART_DNSMASQ_COUNT.value(Integer.class)); if (timeout == null) { - restf.asyncJsonPost(url, cmd, new JsonAsyncRESTCallback(trigger) { + restf.asyncJsonPost(url, cmd, vrMgr.buildAgentCallbackUrlHeaders(mgmtNic.getIp()), new JsonAsyncRESTCallback(trigger) { @Override public void fail(ErrorCode err) { trigger.fail(err); @@ -131,7 +131,7 @@ public Class getReturnClass() { } }); } else { - restf.asyncJsonPost(url, cmd, new JsonAsyncRESTCallback(trigger) { + restf.asyncJsonPost(url, cmd, vrMgr.buildAgentCallbackUrlHeaders(mgmtNic.getIp()), new JsonAsyncRESTCallback(trigger) { @Override public void fail(ErrorCode err) { trigger.fail(err); diff --git a/plugin/virtualRouterProvider/src/main/java/org/zstack/network/service/virtualrouter/vyos/VyosConnectFlow.java b/plugin/virtualRouterProvider/src/main/java/org/zstack/network/service/virtualrouter/vyos/VyosConnectFlow.java index b3a7037e4cc..26f1caf8f76 100755 --- a/plugin/virtualRouterProvider/src/main/java/org/zstack/network/service/virtualrouter/vyos/VyosConnectFlow.java +++ b/plugin/virtualRouterProvider/src/main/java/org/zstack/network/service/virtualrouter/vyos/VyosConnectFlow.java @@ -188,7 +188,7 @@ public void run(final FlowTrigger trigger, Map data) { cmd.setParms(parms); - restf.asyncJsonPost(url, cmd, new JsonAsyncRESTCallback(trigger) { + restf.asyncJsonPost(url, cmd, vrMgr.buildAgentCallbackUrlHeaders(mgmtNic.getIp()), new JsonAsyncRESTCallback(trigger) { @Override public void fail(ErrorCode err) { errs.add(err); From 9d4ddad2389d9486f24d4d07cd691943976d71ac Mon Sep 17 00:00:00 2001 From: "shixin.ruan" Date: Wed, 27 May 2026 18:51:51 +0900 Subject: [PATCH 39/53] [console]: listen on IPv6 proxy host Console proxy returned the MN IPv6 address to clients but selected the listen address from agentIp, which is 127.0.0.1 for the management-node agent. Use the client-facing proxy hostname to choose the wildcard listen address so IPv6 console URLs are reachable. Resolves: ZSTAC-85595 Change-Id: Ief18c9b847f0e0c050ce993a50244533614fd2b8 --- .../src/main/java/org/zstack/console/ConsoleProxyBase.java | 6 +++--- .../test/integration/core/ManagementNetworkIpv6Case.groovy | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/console/src/main/java/org/zstack/console/ConsoleProxyBase.java b/console/src/main/java/org/zstack/console/ConsoleProxyBase.java index 92f3a77b036..bf742b34ec5 100755 --- a/console/src/main/java/org/zstack/console/ConsoleProxyBase.java +++ b/console/src/main/java/org/zstack/console/ConsoleProxyBase.java @@ -82,7 +82,7 @@ private void doEstablishConsoleProxyConnection(ConsoleUrl consoleUrl, final Retu cmd.setTargetSchema(targetSchema); cmd.setTargetHostname(targetHostname); cmd.setTargetPort(targetPort); - cmd.setProxyHostname(selectProxyListenHostname(self.getAgentIp())); + cmd.setProxyHostname(selectProxyListenHostname(self.getProxyHostname())); if (ConsoleConstants.HTTP_SCHEMA.equals(targetSchema)) { cmd.setProxyPort(CoreGlobalProperty.HTTP_CONSOLE_PROXY_PORT); } else { @@ -128,8 +128,8 @@ public Class getReturnClass() { }); } - public static String selectProxyListenHostname(String agentIp) { - return IPv6NetworkUtils.isIpv6Address(agentIp) ? ANY_IPV6_ADDRESS : ANY_IPV4_ADDRESS; + public static String selectProxyListenHostname(String proxyHostname) { + return IPv6NetworkUtils.isIpv6Address(proxyHostname) ? ANY_IPV6_ADDRESS : ANY_IPV4_ADDRESS; } void doEstablishDirectConsoleConnection(ConsoleUrl consoleUrl, final ReturnValueCompletion completion) { diff --git a/test/src/test/groovy/org/zstack/test/integration/core/ManagementNetworkIpv6Case.groovy b/test/src/test/groovy/org/zstack/test/integration/core/ManagementNetworkIpv6Case.groovy index fb640bddb9e..d8ecd0f741a 100644 --- a/test/src/test/groovy/org/zstack/test/integration/core/ManagementNetworkIpv6Case.groovy +++ b/test/src/test/groovy/org/zstack/test/integration/core/ManagementNetworkIpv6Case.groovy @@ -83,7 +83,7 @@ class ManagementNetworkIpv6Case extends SubCase { testBuildUrlIpv6() testLegacyUrlBuilderIpv6() testConsoleVncUriIpv6() - testConsoleProxyListenHostByAgentIpVersion() + testConsoleProxyListenHostByProxyIpVersion() testCoreManagementUrlsIpv6() testRestFacadeIpv6Urls() testSshTargetUsesBracketedIpv6Host() @@ -186,7 +186,7 @@ class ManagementNetworkIpv6Case extends SubCase { assert uri.port == REST_PORT } - void testConsoleProxyListenHostByAgentIpVersion() { + void testConsoleProxyListenHostByProxyIpVersion() { assert ConsoleProxyBase.selectProxyListenHostname(IPV6) == "::" assert ConsoleProxyBase.selectProxyListenHostname(IPV4) == "0.0.0.0" assert ConsoleProxyBase.selectProxyListenHostname("mn.example.com") == "0.0.0.0" From a88c71add95428f8abe8973bcbed0efd039e3637 Mon Sep 17 00:00:00 2001 From: "shixin.ruan" Date: Thu, 28 May 2026 19:45:10 +0900 Subject: [PATCH 40/53] [mgt-ipv6]: document callback interfaces Add Javadoc for newly added callback URL interface methods per review. Resolves: ZSTAC-85504 Change-Id: Ia72206f1275f7c4e4c278bff4fb29e63c8fbf55b --- header/src/main/java/org/zstack/header/rest/RESTFacade.java | 6 ++++++ .../network/service/virtualrouter/VirtualRouterManager.java | 6 ++++++ 2 files changed, 12 insertions(+) diff --git a/header/src/main/java/org/zstack/header/rest/RESTFacade.java b/header/src/main/java/org/zstack/header/rest/RESTFacade.java index 31f4ac15f4e..b686811db63 100755 --- a/header/src/main/java/org/zstack/header/rest/RESTFacade.java +++ b/header/src/main/java/org/zstack/header/rest/RESTFacade.java @@ -88,6 +88,12 @@ public interface RESTFacade { String getCallbackUrl(); + /** + * Builds an async REST callback URL using the specified callback host name or IP address. + * + * @param hostName callback host name or IP address + * @return callback URL reachable by agents + */ String buildCallbackUrl(String hostName); String getHostName(); diff --git a/plugin/virtualRouterProvider/src/main/java/org/zstack/network/service/virtualrouter/VirtualRouterManager.java b/plugin/virtualRouterProvider/src/main/java/org/zstack/network/service/virtualrouter/VirtualRouterManager.java index 17a790f1020..acbad6f0291 100755 --- a/plugin/virtualRouterProvider/src/main/java/org/zstack/network/service/virtualrouter/VirtualRouterManager.java +++ b/plugin/virtualRouterProvider/src/main/java/org/zstack/network/service/virtualrouter/VirtualRouterManager.java @@ -19,6 +19,12 @@ public interface VirtualRouterManager { String buildUrl(String mgmtNicIp, String subPath); + /** + * Builds async REST callback headers for virtual router agent requests. + * + * @param mgmtNicIp virtual router management NIC IP address used to select the callback IP family + * @return HTTP headers carrying the callback URL for agent requests + */ Map buildAgentCallbackUrlHeaders(String mgmtNicIp); List selectL3NetworksNeedingSpecificNetworkService(List candidate, NetworkServiceType nsType); From 123bd2bd9a003234ccc878bff59cb6cbd2669a8d Mon Sep 17 00:00:00 2001 From: "shixin.ruan" Date: Fri, 29 May 2026 20:57:01 +0900 Subject: [PATCH 41/53] [zbs]: support ipv6 endpoints Resolves: ZSTAC-79206 Change-Id: I485bade7bd875e2b492174f4c2e7e710fee1c0b3 --- .../java/org/zstack/storage/zbs/MdsUri.java | 5 +-- .../org/zstack/storage/zbs/ZbsAgentUrl.java | 26 +++++++++------ .../console/ConsoleProxyCase.groovy | 28 ++++++++++++++++ .../addon/zbs/ZbsPrimaryStorageCase.groovy | 33 +++++++++++++++++++ 4 files changed, 80 insertions(+), 12 deletions(-) diff --git a/plugin/zbs/src/main/java/org/zstack/storage/zbs/MdsUri.java b/plugin/zbs/src/main/java/org/zstack/storage/zbs/MdsUri.java index d0c990d6826..0e46313d40a 100644 --- a/plugin/zbs/src/main/java/org/zstack/storage/zbs/MdsUri.java +++ b/plugin/zbs/src/main/java/org/zstack/storage/zbs/MdsUri.java @@ -5,6 +5,7 @@ import org.zstack.header.errorcode.ErrorCode; import org.zstack.header.errorcode.OperationFailureException; import org.zstack.header.exception.CloudRuntimeException; +import org.zstack.utils.network.IPv6NetworkUtils; import java.net.URI; import java.net.URISyntaxException; @@ -26,7 +27,7 @@ public class MdsUri { private String username; private String password; - private static final String MDS_URL_FORMAT = "sshUsername:sshPassword@hostname:[sshPort]/?[mdsPort=]"; + private static final String MDS_URL_FORMAT = "sshUsername:sshPassword@hostname[:sshPort]/?[mdsPort=], IPv6 hostname must be bracketed"; private static final Integer DEFAULT_MDS_PORT = 6666; private static final Integer DEFAULT_SSH_PORT = 22; @@ -80,7 +81,7 @@ public MdsUri(String url) { password = ssh[1]; URI uri = new URI(String.format("ssh://%s", rest)); - hostname = uri.getHost(); + hostname = IPv6NetworkUtils.stripHostUrlBrackets(uri.getHost()); if (hostname == null) { throw new OperationFailureException(operr(ORG_ZSTACK_STORAGE_ZBS_10004, "invalid mdsUrl[%s], hostname cannot be null. A valid mdsUrl is" + " in format of %s", url, MDS_URL_FORMAT) diff --git a/plugin/zbs/src/main/java/org/zstack/storage/zbs/ZbsAgentUrl.java b/plugin/zbs/src/main/java/org/zstack/storage/zbs/ZbsAgentUrl.java index ef42cef1193..34a98f8f8ae 100644 --- a/plugin/zbs/src/main/java/org/zstack/storage/zbs/ZbsAgentUrl.java +++ b/plugin/zbs/src/main/java/org/zstack/storage/zbs/ZbsAgentUrl.java @@ -1,21 +1,27 @@ package org.zstack.storage.zbs; -import org.springframework.web.util.UriComponentsBuilder; +import org.zstack.utils.network.IPv6NetworkUtils; /** * @author Xingwei Yu * @date 2024/3/27 17:39 */ public class ZbsAgentUrl { - public static String primaryStorageUrl(String ip, String path) { - UriComponentsBuilder ub = UriComponentsBuilder.newInstance(); - ub.scheme("http"); - ub.host(ip); - ub.port(ZbsGlobalProperty.PRIMARY_STORAGE_AGENT_PORT); - if (!"".equals(ZbsGlobalProperty.PRIMARY_STORAGE_AGENT_URL_ROOT_PATH)) { - ub.path(ZbsGlobalProperty.PRIMARY_STORAGE_AGENT_URL_ROOT_PATH); + private static void appendPath(StringBuilder sb, String path) { + if (path == null || path.isEmpty()) { + return; + } + + if (!path.startsWith("/")) { + sb.append("/"); } - ub.path(path); - return ub.build().toUriString(); + sb.append(path); + } + + public static String primaryStorageUrl(String ip, String path) { + StringBuilder sb = new StringBuilder(IPv6NetworkUtils.buildHttpUrl(ip, ZbsGlobalProperty.PRIMARY_STORAGE_AGENT_PORT)); + appendPath(sb, ZbsGlobalProperty.PRIMARY_STORAGE_AGENT_URL_ROOT_PATH); + appendPath(sb, path); + return sb.toString(); } } diff --git a/test/src/test/groovy/org/zstack/test/integration/console/ConsoleProxyCase.groovy b/test/src/test/groovy/org/zstack/test/integration/console/ConsoleProxyCase.groovy index 839cb50554a..da26544976d 100644 --- a/test/src/test/groovy/org/zstack/test/integration/console/ConsoleProxyCase.groovy +++ b/test/src/test/groovy/org/zstack/test/integration/console/ConsoleProxyCase.groovy @@ -2,6 +2,7 @@ package org.zstack.test.integration.console import org.springframework.http.HttpEntity import org.zstack.console.ConsoleGlobalConfig +import org.zstack.console.ConsoleManagerImpl import org.zstack.header.vm.VmInstanceConstant import org.zstack.core.CoreGlobalProperty import org.zstack.core.Platform @@ -16,6 +17,7 @@ import org.zstack.header.console.ConsoleProxyVO import org.zstack.header.console.ConsoleProxyVO_ import org.zstack.header.vm.KvmReportVmShutdownFromGuestEventMsg import org.zstack.sdk.ConsoleInventory +import org.zstack.sdk.ConsoleProxyAgentInventory import org.zstack.sdk.GarbageCollectorInventory import org.zstack.sdk.SessionInventory import org.zstack.sdk.VmInstanceInventory @@ -236,6 +238,32 @@ class ConsoleProxyCase extends SubCase { assert agent.consoleProxyOverriddenIp == "127.0.0.1" assert agent.consoleProxyPort == 4900 + String ipv6ConsoleProxyIp = "2001:db8::100" + updateConsoleProxyAgent { + uuid = agent.uuid + consoleProxyOverriddenIp = ipv6ConsoleProxyIp + consoleProxyPort = 4900 + } + + agent = dbf.reload(agent) + assert agent.consoleProxyOverriddenIp == ipv6ConsoleProxyIp + assert Platform.getGlobalProperties().get("consoleProxyOverriddenIp") == ipv6ConsoleProxyIp + assert CoreGlobalProperty.CONSOLE_PROXY_OVERRIDDEN_IP == ipv6ConsoleProxyIp + + List agents = queryConsoleProxyAgent { + conditions = ["uuid=${agent.uuid}".toString()] + } as List + assert agents[0].consoleProxyOverriddenIp == ipv6ConsoleProxyIp + def selectConsoleProxyHostname = ConsoleManagerImpl.class.getDeclaredMethod("selectConsoleProxyHostname", String.class, Boolean.TYPE, String.class) + selectConsoleProxyHostname.accessible = true + assert selectConsoleProxyHostname.invoke(null, ipv6ConsoleProxyIp, false, "127.0.0.1") == "[${ipv6ConsoleProxyIp}]" + + updateConsoleProxyAgent { + uuid = agent.uuid + consoleProxyOverriddenIp = "127.0.0.1" + consoleProxyPort = 4900 + } + // update console proxy agent by none admin account SessionInventory testAccountSession = logInByAccount { accountName = "test" diff --git a/test/src/test/groovy/org/zstack/test/integration/storage/primary/addon/zbs/ZbsPrimaryStorageCase.groovy b/test/src/test/groovy/org/zstack/test/integration/storage/primary/addon/zbs/ZbsPrimaryStorageCase.groovy index e5bfba095e9..5be5ad24392 100644 --- a/test/src/test/groovy/org/zstack/test/integration/storage/primary/addon/zbs/ZbsPrimaryStorageCase.groovy +++ b/test/src/test/groovy/org/zstack/test/integration/storage/primary/addon/zbs/ZbsPrimaryStorageCase.groovy @@ -26,7 +26,9 @@ import org.zstack.header.storage.primary.PrimaryStorageHostStatus import org.zstack.storage.volume.VolumeGlobalConfig import org.zstack.storage.zbs.AddonInfo import org.zstack.storage.zbs.Config +import org.zstack.storage.zbs.ZbsAgentUrl import org.zstack.storage.zbs.ZbsConstants +import org.zstack.storage.zbs.ZbsGlobalProperty import org.zstack.storage.zbs.ZbsPrimaryStorageMdsBase import org.zstack.storage.zbs.ZbsStorageController import org.zstack.test.integration.storage.StorageTest @@ -184,6 +186,7 @@ class ZbsPrimaryStorageCase extends SubCase { testAddExternalPrimaryStorageWithMalformedJsonRejectedByInterceptor() testDataVolumeNegativeScenario() testDecodeMdsUriWithSpecialPassword() + testZbsMdsUriAndAgentUrlSupportIpv6() testMdsReconnectAfterMaximumPingFailures() } } @@ -816,6 +819,36 @@ class ZbsPrimaryStorageCase extends SubCase { assert uri.password == specialPassword } + void testZbsMdsUriAndAgentUrlSupportIpv6() { + def ipv6Uri = new MdsUri("root:password@[2001:db8::10]:2222/?mdsPort=7777") + assert ipv6Uri.hostname == "2001:db8::10" + assert ipv6Uri.sshPort == 2222 + assert ipv6Uri.mdsPort == 7777 + assert ZbsAgentUrl.primaryStorageUrl(ipv6Uri.hostname, ZbsPrimaryStorageMdsBase.PING_PATH) == + "http://[2001:db8::10]:${ZbsGlobalProperty.PRIMARY_STORAGE_AGENT_PORT}${ZbsPrimaryStorageMdsBase.PING_PATH}" + + def ipv4Uri = new MdsUri("root:password@172.24.249.182:2222/?mdsPort=7777") + assert ipv4Uri.hostname == "172.24.249.182" + assert ZbsAgentUrl.primaryStorageUrl(ipv4Uri.hostname, ZbsPrimaryStorageMdsBase.PING_PATH) == + "http://172.24.249.182:${ZbsGlobalProperty.PRIMARY_STORAGE_AGENT_PORT}${ZbsPrimaryStorageMdsBase.PING_PATH}" + + def endpoints = [ + "http://172.24.249.182:7763${ZbsPrimaryStorageMdsBase.PING_PATH}", + ZbsAgentUrl.primaryStorageUrl(ipv6Uri.hostname, ZbsPrimaryStorageMdsBase.PING_PATH) + ] + String ipv6PingUrl = "http://[2001:db8::10]:${ZbsGlobalProperty.PRIMARY_STORAGE_AGENT_PORT}${ZbsPrimaryStorageMdsBase.PING_PATH}" + assert endpoints.contains(ipv6PingUrl) + + String oldRootPath = ZbsGlobalProperty.PRIMARY_STORAGE_AGENT_URL_ROOT_PATH + try { + ZbsGlobalProperty.PRIMARY_STORAGE_AGENT_URL_ROOT_PATH = "zstack" + assert ZbsAgentUrl.primaryStorageUrl(ipv6Uri.hostname, ZbsPrimaryStorageMdsBase.PING_PATH) == + "http://[2001:db8::10]:${ZbsGlobalProperty.PRIMARY_STORAGE_AGENT_PORT}/zstack${ZbsPrimaryStorageMdsBase.PING_PATH}" + } finally { + ZbsGlobalProperty.PRIMARY_STORAGE_AGENT_URL_ROOT_PATH = oldRootPath + } + } + void testMdsReconnectAfterMaximumPingFailures() { env.cleanSimulatorAndMessageHandlers() Integer originalPingInterval = PrimaryStorageGlobalConfig.PING_INTERVAL.value().toInteger() From d84840d31c7c9c2aa37b5447f4fb50fb5fb91fe0 Mon Sep 17 00:00:00 2001 From: "shixin.ruan" Date: Sat, 30 May 2026 01:05:07 +0900 Subject: [PATCH 42/53] [mgt-ipv6]: allow system ipv6 l3 range Fixes ZSTAC-85690.\n\nAllow IPv6 ranges on System L3 networks for management network IPv6.\nAlso allow virtual router offerings to use IPv6 management L3. Change-Id: If46c0ab86c018f72d0138df6363689f85dbecaeb --- .../java/org/zstack/network/l3/L3NetworkApiInterceptor.java | 4 ---- .../service/virtualrouter/VirtualRouterApiInterceptor.java | 5 ----- 2 files changed, 9 deletions(-) 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 ac88e09dbf8..f5717441a68 100755 --- a/network/src/main/java/org/zstack/network/l3/L3NetworkApiInterceptor.java +++ b/network/src/main/java/org/zstack/network/l3/L3NetworkApiInterceptor.java @@ -445,10 +445,6 @@ private void validateIpv6Range(IpRangeInventory ipr) { L3NetworkVO l3Vo = Q.New(L3NetworkVO.class).eq(L3NetworkVO_.uuid, ipr.getL3NetworkUuid()).find(); - if (l3Vo.getCategory().equals(L3NetworkCategory.System)) { - throw new ApiMessageInterceptionException(argerr(ORG_ZSTACK_NETWORK_L3_10034, "can not add ip range, because system network doesn't support ipv6 yet")); - } - List rangeVOS = Q.New(NormalIpRangeVO.class).eq(NormalIpRangeVO_.l3NetworkUuid, ipr.getL3NetworkUuid()).eq(NormalIpRangeVO_.ipVersion, IPv6Constants.IPv6).list(); if (rangeVOS != null && !rangeVOS.isEmpty()) { if (!rangeVOS.get(0).getAddressMode().equals(ipr.getAddressMode())) { diff --git a/plugin/virtualRouterProvider/src/main/java/org/zstack/network/service/virtualrouter/VirtualRouterApiInterceptor.java b/plugin/virtualRouterProvider/src/main/java/org/zstack/network/service/virtualrouter/VirtualRouterApiInterceptor.java index ea2e5ba66bb..2713a4699cc 100755 --- a/plugin/virtualRouterProvider/src/main/java/org/zstack/network/service/virtualrouter/VirtualRouterApiInterceptor.java +++ b/plugin/virtualRouterProvider/src/main/java/org/zstack/network/service/virtualrouter/VirtualRouterApiInterceptor.java @@ -212,11 +212,6 @@ private void validate(APICreateVirtualRouterOfferingMsg msg) { throw new ApiMessageInterceptionException(argerr(ORG_ZSTACK_NETWORK_SERVICE_VIRTUALROUTER_10030, "management network[uuid:%s] is not in the same zone[uuid:%s] this offering is going to create", msg.getManagementNetworkUuid(), msg.getZoneUuid())); } - /* mgt network does not support ipv6 yet, TODO, will be implemented soon */ - if (mgtL3.getIpVersions().contains(IPv6Constants.IPv6) && !mgtL3.getIpVersions().contains(IPv6Constants.IPv4)) { - throw new ApiMessageInterceptionException(argerr(ORG_ZSTACK_NETWORK_SERVICE_VIRTUALROUTER_10031, "can not create virtual router offering, because management network doesn't support ipv6 yet")); - } - if (!CoreGlobalProperty.UNIT_TEST_ON) { checkIfManagementNetworkReachable(msg.getManagementNetworkUuid()); } From e40f7f8d9ce5c682f1f9ca69c7ae7fd65d197aa8 Mon Sep 17 00:00:00 2001 From: "shixin.ruan" Date: Sat, 30 May 2026 09:24:03 +0900 Subject: [PATCH 43/53] [utils]: parse ipv6 system tag tokens Parse system tag delimiters only outside token braces when matching actual tag values. Resolves: ZSTAC-85618 Change-Id: I7d0a9cf3146a513e56ba0d6d3bff3685bc4716e0 --- .../main/java/org/zstack/utils/TagUtils.java | 20 ++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/utils/src/main/java/org/zstack/utils/TagUtils.java b/utils/src/main/java/org/zstack/utils/TagUtils.java index 5058615062d..33e41aab68f 100755 --- a/utils/src/main/java/org/zstack/utils/TagUtils.java +++ b/utils/src/main/java/org/zstack/utils/TagUtils.java @@ -84,7 +84,7 @@ private static List splitTagFieldsByFormat(List fmtFields, Strin continue; } - int end = tag.indexOf(TAG_DELIMITER, offset); + int end = indexOfDelimiterOutsideToken(tag, offset); if (end < 0) { return null; } @@ -145,6 +145,24 @@ private static List splitTagFields(String tag) { return fields; } + private static int indexOfDelimiterOutsideToken(String tag, int offset) { + int braceDepth = 0; + for (int i = offset; i < tag.length(); i++) { + char current = tag.charAt(i); + if (current == TOKEN_START) { + braceDepth++; + } else if (current == TOKEN_END && braceDepth > 0) { + braceDepth--; + } + + if (braceDepth == 0 && tag.startsWith(TAG_DELIMITER, i)) { + return i; + } + } + + return -1; + } + public static Map parseIfMatch(String fmt, String tag) { if (!isMatch(fmt, tag)) { return null; From 2b2d2fb91d6b42032ff071ee481b9fbf7e97bcbb Mon Sep 17 00:00:00 2001 From: "shixin.ruan" Date: Sat, 30 May 2026 12:27:41 +0900 Subject: [PATCH 44/53] [vrouter]: support IPv6 vrouter agent URL Format IPv6 vRouter agent URLs with brackets and select the MN callback source IP from the route to the agent.\n\nAlso expose local non-loopback IPs to appliance VM bootstrap selection so dual-stack MNs can provide an IPv6 managementNodeIp while keeping management.server.ip on IPv4. Resolves: ZSTAC-85692 Change-Id: If1612db0b39389084a0501fa1065b15cac1d9990 --- .../main/java/org/zstack/core/Platform.java | 61 +++++++++++++++++++ .../VirtualRouterManagerImpl.java | 7 ++- 2 files changed, 67 insertions(+), 1 deletion(-) diff --git a/core/src/main/java/org/zstack/core/Platform.java b/core/src/main/java/org/zstack/core/Platform.java index cc2020b22f2..307a0f8dbcf 100755 --- a/core/src/main/java/org/zstack/core/Platform.java +++ b/core/src/main/java/org/zstack/core/Platform.java @@ -1065,10 +1065,71 @@ public static List getManagementServerIps() { ips.add(getManagementServerIp()); ips.add(getManagementServerIp4()); ips.add(getManagementServerIp6()); + ips.addAll(getLocalNonLoopbackIps()); ips.remove(null); return new ArrayList<>(ips); } + private static List getLocalNonLoopbackIps() { + List ips = new ArrayList<>(); + try { + Enumeration nets = NetworkInterface.getNetworkInterfaces(); + for (NetworkInterface iface : Collections.list(nets)) { + if (!iface.isUp()) { + continue; + } + for (InetAddress address : Collections.list(iface.getInetAddresses())) { + if (address.isLoopbackAddress() || address.isLinkLocalAddress()) { + continue; + } + ips.add(normalizeManagementIp(address.getHostAddress())); + } + } + } catch (SocketException e) { + logger.warn("failed to list local non-loopback IPs", e); + } + return ips; + } + + public static String getRouteSourceIp(String remoteIp) { + if (StringUtils.isBlank(remoteIp)) { + return null; + } + + remoteIp = normalizeManagementIp(remoteIp); + String family; + if (IPv6NetworkUtils.isIpv6Address(remoteIp)) { + family = "-6"; + } else if (NetworkUtils.isIpv4Address(remoteIp)) { + family = "-4"; + } else { + return null; + } + + Linux.ShellResult ret = Linux.shell(String.format("/sbin/ip %s route get %s", family, remoteIp)); + if (ret.getExitCode() != 0) { + logger.warn(String.format("failed to get route source IP for remote[%s], stdout[%s], stderr[%s]", + remoteIp, ret.getStdout(), ret.getStderr())); + return null; + } + + String[] tokens = ret.getStdout().trim().split("\\s+"); + for (int i = 0; i < tokens.length - 1; i++) { + if (!"src".equals(tokens[i])) { + continue; + } + String sourceIp = normalizeManagementIp(tokens[i + 1]); + if (IPv6NetworkUtils.isIpv6Address(remoteIp) && IPv6NetworkUtils.isIpv6Address(sourceIp)) { + return sourceIp; + } + if (NetworkUtils.isIpv4Address(remoteIp) && NetworkUtils.isIpv4Address(sourceIp)) { + return sourceIp; + } + } + + return null; + } + public static String selectManagementServerIp(Collection addresses) { String ipv4 = null; String ipv6 = null; 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 3e435674ae7..b3a48429692 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 @@ -960,7 +960,7 @@ public String buildUrl(String mgmtNicIp, String subPath) { if (CoreGlobalProperty.UNIT_TEST_ON) { ub.host("localhost"); } else { - ub.host(mgmtNicIp); + ub.host(IPv6NetworkUtils.formatHostForUrl(mgmtNicIp)); } ub.port(VirtualRouterGlobalProperty.AGENT_PORT); @@ -978,6 +978,11 @@ public Map buildAgentCallbackUrlHeaders(String mgmtNicIp) { } private String selectManagementIpForAgent(String agentIp) { + String routeSourceIp = Platform.getRouteSourceIp(agentIp); + if (routeSourceIp != null) { + return routeSourceIp; + } + if (IPv6NetworkUtils.isIpv6Address(agentIp)) { return Platform.getManagementServerIps().stream() .filter(IPv6NetworkUtils::isIpv6Address) From 9c58c88c326afd02cb239da3e1045e8f03ccbdeb Mon Sep 17 00:00:00 2001 From: "shixin.ruan" Date: Sat, 30 May 2026 13:31:57 +0900 Subject: [PATCH 45/53] [mgt-ipv6]: scope mn ip fallback Keep getManagementServerIps narrow and use explicit local fallback only for appliance VM bootstrap CIDR selection. Resolves: ZSTAC-79206 Change-Id: Ib90e405fcef57b0dcbe03703ddcf2fe92682745b --- core/src/main/java/org/zstack/core/Platform.java | 6 ++++++ .../java/org/zstack/appliancevm/ApplianceVmFacadeImpl.java | 2 +- .../utils/clouderrorcode/CloudOperationsErrorCode.java | 4 ++++ 3 files changed, 11 insertions(+), 1 deletion(-) diff --git a/core/src/main/java/org/zstack/core/Platform.java b/core/src/main/java/org/zstack/core/Platform.java index 307a0f8dbcf..2a4ba9631a7 100755 --- a/core/src/main/java/org/zstack/core/Platform.java +++ b/core/src/main/java/org/zstack/core/Platform.java @@ -1065,6 +1065,12 @@ public static List getManagementServerIps() { ips.add(getManagementServerIp()); ips.add(getManagementServerIp4()); ips.add(getManagementServerIp6()); + ips.remove(null); + return new ArrayList<>(ips); + } + + public static List getManagementServerIpsWithLocalFallback() { + LinkedHashSet ips = new LinkedHashSet<>(getManagementServerIps()); ips.addAll(getLocalNonLoopbackIps()); ips.remove(null); return new ArrayList<>(ips); diff --git a/plugin/applianceVm/src/main/java/org/zstack/appliancevm/ApplianceVmFacadeImpl.java b/plugin/applianceVm/src/main/java/org/zstack/appliancevm/ApplianceVmFacadeImpl.java index 99159d643bf..cf17c2df318 100755 --- a/plugin/applianceVm/src/main/java/org/zstack/appliancevm/ApplianceVmFacadeImpl.java +++ b/plugin/applianceVm/src/main/java/org/zstack/appliancevm/ApplianceVmFacadeImpl.java @@ -467,7 +467,7 @@ public Map prepareBootstrapInformation(VmInstanceSpec spec) { ret.put(ApplianceVmConstant.BootstrapParams.publicKey.toString(), publicKey); ret.put(BootstrapParams.uuid.toString(), spec.getVmInventory().getUuid()); putManagementNodeBootstrapParams(ret, - Platform.getManagementServerIps(), + Platform.getManagementServerIpsWithLocalFallback(), getVrManagementCidrs(mgmtNic), Platform.getManagementServerIp(), Platform.getManagementServerVip(), diff --git a/utils/src/main/java/org/zstack/utils/clouderrorcode/CloudOperationsErrorCode.java b/utils/src/main/java/org/zstack/utils/clouderrorcode/CloudOperationsErrorCode.java index 3a272354846..98fb901e470 100644 --- a/utils/src/main/java/org/zstack/utils/clouderrorcode/CloudOperationsErrorCode.java +++ b/utils/src/main/java/org/zstack/utils/clouderrorcode/CloudOperationsErrorCode.java @@ -13097,6 +13097,10 @@ public class CloudOperationsErrorCode { public static final String ORG_ZSTACK_VPC_HA_10020 = "ORG_ZSTACK_VPC_HA_10020"; + public static final String ORG_ZSTACK_VPC_HA_10021 = "ORG_ZSTACK_VPC_HA_10021"; + + public static final String ORG_ZSTACK_VPC_HA_10022 = "ORG_ZSTACK_VPC_HA_10022"; + public static final String ORG_ZSTACK_SSO_CAS_FILTER_10000 = "ORG_ZSTACK_SSO_CAS_FILTER_10000"; public static final String ORG_ZSTACK_SSO_CAS_FILTER_10001 = "ORG_ZSTACK_SSO_CAS_FILTER_10001"; From 21e2dbcc5c66998c3676219faafcc6892cc3d1aa Mon Sep 17 00:00:00 2001 From: "shixin.ruan" Date: Sun, 31 May 2026 19:31:09 +0900 Subject: [PATCH 46/53] [kvm]: redeploy tls cert for extra ips ReconnectHost TLS cert SAN check now includes Host extraIps before choosing whether to redeploy certs. This keeps IPv6 migration extraIps covered by qemu/libvirt certificates. Resolves: ZSTAC-85638 Change-Id: I41c45203d606742bec0085af60f64e7bf085cf12 --- plugin/kvm/src/main/java/org/zstack/kvm/KVMHost.java | 4 +++- .../test/java/org/zstack/test/kvm/KVMHostUtilsTest.java | 7 +++++++ 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/plugin/kvm/src/main/java/org/zstack/kvm/KVMHost.java b/plugin/kvm/src/main/java/org/zstack/kvm/KVMHost.java index 36e53287727..26b48347b1e 100755 --- a/plugin/kvm/src/main/java/org/zstack/kvm/KVMHost.java +++ b/plugin/kvm/src/main/java/org/zstack/kvm/KVMHost.java @@ -5753,7 +5753,9 @@ public void run(FlowTrigger trigger, Map data) { // expectation matches what the host itself reports. String certIpList = KVMHostUtils.collectHostIps( sshShell, self.getUuid(), managementIp); - List allIps = new ArrayList<>(Arrays.asList(certIpList.split(","))); + String expectedCertIpList = KVMHostUtils.unionTlsCertIps( + self.getUuid(), managementIp, certIpList); + List allIps = new ArrayList<>(Arrays.asList(expectedCertIpList.split(","))); // Save detected IPs so apply-ansible-playbook can union with // EXTRA_IPS without running a second SSH. data.put("TLS_DETECTED_IPS", certIpList); diff --git a/test/src/test/java/org/zstack/test/kvm/KVMHostUtilsTest.java b/test/src/test/java/org/zstack/test/kvm/KVMHostUtilsTest.java index 6933d2e9db2..19350452cbe 100644 --- a/test/src/test/java/org/zstack/test/kvm/KVMHostUtilsTest.java +++ b/test/src/test/java/org/zstack/test/kvm/KVMHostUtilsTest.java @@ -123,6 +123,13 @@ public void unionIps_detectedDeduplicatesExtra() { "10.0.0.7,10.0.0.8", null, Collections.emptySet())); } + @Test + public void unionIps_keepsIpv6ExtraIpsForTlsCert() { + Assert.assertEquals("172.25.115.196,172.26.115.213,2026:3:3:1::26:7f3e", + KVMHostUtils.unionIps("172.25.115.196,172.26.115.213", "172.25.115.196", + "172.26.115.213,2026:3:3:1::26:7f3e", null, Collections.emptySet())); + } + @Test public void unionIps_noExtraTagYieldsDetectedOnly() { Assert.assertEquals("192.168.1.10,172.17.0.1", From 9026caddc6d365b4181bbf7311d1f7851706e853 Mon Sep 17 00:00:00 2001 From: "shixin.ruan" Date: Mon, 1 Jun 2026 19:21:10 +0900 Subject: [PATCH 47/53] [mgt-ipv6]: reject mixed management ranges Only System L3 management networks reject mixed IPv4 and IPv6 ranges. Normal service L3 dual-stack behavior is unchanged. Resolves: ZSTAC-85709 Change-Id: I2076e6ab924848bbfc509bea1477c82017c18ecf --- .../network/l3/L3NetworkApiInterceptor.java | 25 +++++++++ .../l3network/ipv6/Ipv6RangeCase.groovy | 56 ++++++++++++++++++- .../CloudOperationsErrorCode.java | 2 + 3 files changed, 82 insertions(+), 1 deletion(-) 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 f5717441a68..6a16cee1e3a 100755 --- a/network/src/main/java/org/zstack/network/l3/L3NetworkApiInterceptor.java +++ b/network/src/main/java/org/zstack/network/l3/L3NetworkApiInterceptor.java @@ -444,6 +444,7 @@ private void validateIpv6Range(IpRangeInventory ipr) { } L3NetworkVO l3Vo = Q.New(L3NetworkVO.class).eq(L3NetworkVO_.uuid, ipr.getL3NetworkUuid()).find(); + validateManagementNetworkIpRangeVersion(l3Vo, IPv6Constants.IPv6); List rangeVOS = Q.New(NormalIpRangeVO.class).eq(NormalIpRangeVO_.l3NetworkUuid, ipr.getL3NetworkUuid()).eq(NormalIpRangeVO_.ipVersion, IPv6Constants.IPv6).list(); if (rangeVOS != null && !rangeVOS.isEmpty()) { @@ -622,6 +623,7 @@ private void validateAddressPool(IpRangeInventory ipr) { private void validate(IpRangeInventory ipr) { L3NetworkVO l3Vo = Q.New(L3NetworkVO.class).eq(L3NetworkVO_.uuid, ipr.getL3NetworkUuid()).find(); + validateManagementNetworkIpRangeVersion(l3Vo, IPv6Constants.IPv4); if (ipr.getIpRangeType() == IpRangeType.AddressPool && l3Vo.getCategory() != L3NetworkCategory.Public) { throw new ApiMessageInterceptionException(argerr(ORG_ZSTACK_NETWORK_L3_10049, "l3 network [uuid %s: name %s] is not a public network, address pool range can not be added", l3Vo.getUuid(), l3Vo.getName())); @@ -769,6 +771,29 @@ private void validate(IpRangeInventory ipr) { } } + private void validateManagementNetworkIpRangeVersion(L3NetworkVO l3Vo, int newIpVersion) { + if (l3Vo.getCategory() != L3NetworkCategory.System) { + return; + } + + int existingIpVersion = newIpVersion == IPv6Constants.IPv4 ? IPv6Constants.IPv6 : IPv6Constants.IPv4; + boolean hasExistingIpRange = Q.New(IpRangeVO.class) + .eq(IpRangeVO_.l3NetworkUuid, l3Vo.getUuid()) + .eq(IpRangeVO_.ipVersion, existingIpVersion) + .isExists(); + if (!hasExistingIpRange) { + return; + } + + throw new ApiMessageInterceptionException(argerr(ORG_ZSTACK_NETWORK_L3_10082, + "management network l3[uuid:%s] cannot mix IPv4 and IPv6 IP ranges; existing IP version is %s, new IP version is %s", + l3Vo.getUuid(), getIpVersionName(existingIpVersion), getIpVersionName(newIpVersion))); + } + + private String getIpVersionName(int ipVersion) { + return ipVersion == IPv6Constants.IPv6 ? "IPv6" : "IPv4"; + } + private void validate(APIAddIpRangeMsg msg) { if (msg.getIpRangeType() == null) { msg.setIpRangeType(IpRangeType.Normal.toString()); diff --git a/test/src/test/groovy/org/zstack/test/integration/network/l3network/ipv6/Ipv6RangeCase.groovy b/test/src/test/groovy/org/zstack/test/integration/network/l3network/ipv6/Ipv6RangeCase.groovy index fe0c8c07818..c805abfc2ba 100644 --- a/test/src/test/groovy/org/zstack/test/integration/network/l3network/ipv6/Ipv6RangeCase.groovy +++ b/test/src/test/groovy/org/zstack/test/integration/network/l3network/ipv6/Ipv6RangeCase.groovy @@ -3,6 +3,8 @@ package org.zstack.test.integration.network.l3network.ipv6 import org.zstack.header.network.service.NetworkServiceType import org.zstack.network.service.eip.EipConstant import org.zstack.network.service.portforwarding.PortForwardingConstant +import org.zstack.sdk.AddIpRangeByNetworkCidrAction +import org.zstack.sdk.AddIpv6RangeByNetworkCidrAction import org.zstack.sdk.ImageInventory import org.zstack.sdk.InstanceOfferingInventory import org.zstack.sdk.IpRangeInventory @@ -15,6 +17,7 @@ import org.zstack.test.integration.network.NetworkTest import org.zstack.test.integration.network.l3network.Env import org.zstack.testlib.EnvSpec import org.zstack.testlib.SubCase +import org.zstack.utils.clouderrorcode.CloudOperationsErrorCode import org.zstack.utils.network.IPv6Constants import static java.util.Arrays.asList @@ -52,6 +55,7 @@ class Ipv6RangeCase extends SubCase { testAttachIpv6RangeAddressMode() testIpv6RangeWith2Ips() testIpv6RangeLastAddress() + testManagementNetworkRejectMixedIpRanges() } } @@ -500,5 +504,55 @@ class Ipv6RangeCase extends SubCase { addressMode = IPv6Constants.Stateful_DHCP } } -} + void testManagementNetworkRejectMixedIpRanges() { + L2NetworkInventory l2 = env.inventoryByName("l2") + + L3NetworkInventory ipv4ManagementL3 = createL3Network { + category = "System" + system = true + l2NetworkUuid = l2.uuid + name = "system-ipv4" + } + + addIpRangeByNetworkCidr { + name = "system-ipv4-range" + l3NetworkUuid = ipv4ManagementL3.uuid + networkCidr = "10.10.10.0/24" + } + + AddIpv6RangeByNetworkCidrAction addIpv6RangeAction = new AddIpv6RangeByNetworkCidrAction() + addIpv6RangeAction.name = "system-ipv6-range" + addIpv6RangeAction.l3NetworkUuid = ipv4ManagementL3.uuid + addIpv6RangeAction.networkCidr = "2005:2001::/64" + addIpv6RangeAction.addressMode = IPv6Constants.Stateful_DHCP + addIpv6RangeAction.sessionId = adminSession() + AddIpv6RangeByNetworkCidrAction.Result addIpv6RangeResult = addIpv6RangeAction.call() + assert addIpv6RangeResult.error != null + assert addIpv6RangeResult.error.globalErrorCode == CloudOperationsErrorCode.ORG_ZSTACK_NETWORK_L3_10082 + + L3NetworkInventory ipv6ManagementL3 = createL3Network { + category = "System" + system = true + l2NetworkUuid = l2.uuid + name = "system-ipv6" + ipVersion = 6 + } + + addIpv6RangeByNetworkCidr { + name = "system-ipv6-range" + l3NetworkUuid = ipv6ManagementL3.uuid + networkCidr = "2006:2001::/64" + addressMode = IPv6Constants.Stateful_DHCP + } + + AddIpRangeByNetworkCidrAction addIpRangeAction = new AddIpRangeByNetworkCidrAction() + addIpRangeAction.name = "system-ipv4-range" + addIpRangeAction.l3NetworkUuid = ipv6ManagementL3.uuid + addIpRangeAction.networkCidr = "10.10.11.0/24" + addIpRangeAction.sessionId = adminSession() + AddIpRangeByNetworkCidrAction.Result addIpRangeResult = addIpRangeAction.call() + assert addIpRangeResult.error != null + assert addIpRangeResult.error.globalErrorCode == CloudOperationsErrorCode.ORG_ZSTACK_NETWORK_L3_10082 + } +} diff --git a/utils/src/main/java/org/zstack/utils/clouderrorcode/CloudOperationsErrorCode.java b/utils/src/main/java/org/zstack/utils/clouderrorcode/CloudOperationsErrorCode.java index 98fb901e470..733b3e482a9 100644 --- a/utils/src/main/java/org/zstack/utils/clouderrorcode/CloudOperationsErrorCode.java +++ b/utils/src/main/java/org/zstack/utils/clouderrorcode/CloudOperationsErrorCode.java @@ -978,6 +978,8 @@ public class CloudOperationsErrorCode { public static final String ORG_ZSTACK_NETWORK_L3_10081 = "ORG_ZSTACK_NETWORK_L3_10081"; + public static final String ORG_ZSTACK_NETWORK_L3_10082 = "ORG_ZSTACK_NETWORK_L3_10082"; + public static final String ORG_ZSTACK_SNS_PLATFORM_UNIVERSALSMS_SUPPLIER_EMAY_10000 = "ORG_ZSTACK_SNS_PLATFORM_UNIVERSALSMS_SUPPLIER_EMAY_10000"; public static final String ORG_ZSTACK_CORE_VALIDATION_10000 = "ORG_ZSTACK_CORE_VALIDATION_10000"; From 0d22c170098b1e538ed8905f39bb1c4b3c95eeb5 Mon Sep 17 00:00:00 2001 From: "shixin.ruan" Date: Fri, 5 Jun 2026 14:31:41 +0900 Subject: [PATCH 48/53] [mgt-ipv6]: support confirmed ip family change Jira: ZSTAC-85711 Change-Id: I43bc2b35d033f2f82ee2c7e6c6b43dcc2dfcbf82 --- .../main/java/org/zstack/core/Platform.java | 20 +++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/core/src/main/java/org/zstack/core/Platform.java b/core/src/main/java/org/zstack/core/Platform.java index 2a4ba9631a7..a100772ad2d 100755 --- a/core/src/main/java/org/zstack/core/Platform.java +++ b/core/src/main/java/org/zstack/core/Platform.java @@ -83,6 +83,8 @@ public class Platform { private static MessageSource messageSource; private static String encryptionKey = EncryptRSA.generateKeyString("ZStack open source"); private static final String MANAGEMENT_SERVER_IP_PROPERTY = "management.server.ip"; + private static final String MANAGEMENT_SERVER_IP4_PROPERTY = "management.server.ip4"; + private static final String MANAGEMENT_SERVER_IP6_PROPERTY = "management.server.ip6"; private static final String ZSTACK_MANAGEMENT_SERVER_IP_ENV = "ZSTACK_MANAGEMENT_SERVER_IP"; private static final String IPV4_ADDRESS_COMMAND = "ip -4 add"; private static final String IPV6_ADDRESS_COMMAND = "ip -6 addr"; @@ -1009,13 +1011,31 @@ private static String getManagementServerIpInternal() { } public static String getManagementServerIp6() { + String ip = getManagementServerSecondaryIpProperty(MANAGEMENT_SERVER_IP6_PROPERTY); + if (ip != null) { + return ip; + } return getManagementServerIpOnManagementInterface(IPv6Constants.IPv6); } private static String getManagementServerIp4() { + String ip = getManagementServerSecondaryIpProperty(MANAGEMENT_SERVER_IP4_PROPERTY); + if (ip != null) { + return ip; + } return getManagementServerIpOnManagementInterface(IPv6Constants.IPv4); } + private static String getManagementServerSecondaryIpProperty(String property) { + String ip = System.getProperty(property); + if (ip == null || ip.trim().isEmpty()) { + return null; + } + + logger.info(String.format("get management IP[%s] from Java property[%s]", ip, property)); + return normalizeManagementIp(ip); + } + private static String getManagementServerIpOnManagementInterface(int ipVersion) { try { NetworkInterface iface = findManagementServerInterface(); From bc8a1b9e6bfb728d51dccda43baf590fa4dc4d57 Mon Sep 17 00:00:00 2001 From: "shixin.ruan" Date: Fri, 5 Jun 2026 15:16:38 +0900 Subject: [PATCH 49/53] [mgt-ipv6]: validate secondary mn ip family Jira: ZSTAC-85711 Verify: - mvn -pl core -DskipTests compile - mvnTest -Dtest=ManagementNetworkIpv6Case blocked before test methods by local GlobalConfig transaction startup error Change-Id: I0f3c094abf49b99d9104dbbf16a64da83c77ed03 --- .../main/java/org/zstack/core/Platform.java | 16 +++-- .../core/ManagementNetworkIpv6Case.groovy | 59 +++++++++++++++++++ 2 files changed, 71 insertions(+), 4 deletions(-) diff --git a/core/src/main/java/org/zstack/core/Platform.java b/core/src/main/java/org/zstack/core/Platform.java index a100772ad2d..52faca5267f 100755 --- a/core/src/main/java/org/zstack/core/Platform.java +++ b/core/src/main/java/org/zstack/core/Platform.java @@ -1011,7 +1011,7 @@ private static String getManagementServerIpInternal() { } public static String getManagementServerIp6() { - String ip = getManagementServerSecondaryIpProperty(MANAGEMENT_SERVER_IP6_PROPERTY); + String ip = getManagementServerSecondaryIpProperty(MANAGEMENT_SERVER_IP6_PROPERTY, IPv6Constants.IPv6); if (ip != null) { return ip; } @@ -1019,21 +1019,29 @@ public static String getManagementServerIp6() { } private static String getManagementServerIp4() { - String ip = getManagementServerSecondaryIpProperty(MANAGEMENT_SERVER_IP4_PROPERTY); + String ip = getManagementServerSecondaryIpProperty(MANAGEMENT_SERVER_IP4_PROPERTY, IPv6Constants.IPv4); if (ip != null) { return ip; } return getManagementServerIpOnManagementInterface(IPv6Constants.IPv4); } - private static String getManagementServerSecondaryIpProperty(String property) { + private static String getManagementServerSecondaryIpProperty(String property, int ipVersion) { String ip = System.getProperty(property); if (ip == null || ip.trim().isEmpty()) { return null; } + String normalizedIp = normalizeManagementIp(ip); + if ((ipVersion == IPv6Constants.IPv6 && !IPv6NetworkUtils.isIpv6Address(normalizedIp)) || + (ipVersion == IPv6Constants.IPv4 && !NetworkUtils.isIpv4Address(normalizedIp))) { + throw new CloudRuntimeException(String.format( + "management IP[%s] from Java property[%s] is not IPv%s", + ip, property, ipVersion)); + } + logger.info(String.format("get management IP[%s] from Java property[%s]", ip, property)); - return normalizeManagementIp(ip); + return normalizedIp; } private static String getManagementServerIpOnManagementInterface(int ipVersion) { diff --git a/test/src/test/groovy/org/zstack/test/integration/core/ManagementNetworkIpv6Case.groovy b/test/src/test/groovy/org/zstack/test/integration/core/ManagementNetworkIpv6Case.groovy index 463a4599e84..414d9dd09b6 100644 --- a/test/src/test/groovy/org/zstack/test/integration/core/ManagementNetworkIpv6Case.groovy +++ b/test/src/test/groovy/org/zstack/test/integration/core/ManagementNetworkIpv6Case.groovy @@ -5,6 +5,7 @@ import org.zstack.appliancevm.ApplianceVmFacadeImpl import org.zstack.core.ansible.CallBackNetworkChecker import org.zstack.core.ansible.AnsibleRunner import org.zstack.core.Platform +import org.zstack.header.exception.CloudRuntimeException import org.zstack.core.agent.AgentManagerImpl import org.zstack.core.cloudbus.CloudBusImpl3 import org.zstack.core.rest.RESTFacadeImpl @@ -29,6 +30,7 @@ import org.zstack.utils.network.IPv6NetworkUtils import org.zstack.utils.network.NetworkUtils import org.junit.Test +import java.lang.reflect.Field import java.util.function.Supplier class ManagementNetworkIpv6Case extends SubCase { @@ -287,6 +289,33 @@ class ManagementNetworkIpv6Case extends SubCase { assert Platform.getManagementServerCidr(IPv6Constants.IPv4) == Platform.getManagementServerCidr(Platform.getManagementServerIp()) } + void testManagementServerIpsReadSecondaryProperties() { + withManagementServerIpProperties([ + "management.server.ip" : IPV6, + "management.server.ip4": IPV4, + ]) { + assert Platform.getManagementServerIps() == [IPV6, IPV4] + } + + withManagementServerIpProperties([ + "management.server.ip" : IPV4, + "management.server.ip6": IPV6, + ]) { + assert Platform.getManagementServerIps() == [IPV4, IPV6] + } + } + + void testManagementServerSecondaryPropertyRejectsWrongAddressFamily() { + withManagementServerIpProperties([ + "management.server.ip" : IPV4, + "management.server.ip6": IPV4, + ]) { + expect(CloudRuntimeException.class) { + Platform.getManagementServerIps() + } + } + } + void testManagementServerIdPersisted() { String oldValue = System.getProperty(Platform.MANAGEMENT_SERVER_ID_PROPERTY) File propertiesFile = File.createTempFile("zstack-management-server-id", ".properties") @@ -330,6 +359,36 @@ class ManagementNetworkIpv6Case extends SubCase { } } + private void withManagementServerIpProperties(Map properties, Closure closure) { + Map oldValues = [:] + properties.keySet().each { key -> + oldValues[key] = System.getProperty(key) + } + + try { + resetCachedManagementServerIp() + properties.each { key, value -> + System.setProperty(key, value) + } + closure.call() + } finally { + properties.keySet().each { key -> + if (oldValues[key] == null) { + System.clearProperty(key) + } else { + System.setProperty(key, oldValues[key]) + } + } + resetCachedManagementServerIp() + } + } + + private void resetCachedManagementServerIp() { + Field field = Platform.class.getDeclaredField("managementServerIp") + field.setAccessible(true) + field.set(null, null) + } + void testNfsIpv6UrlParsing() { assert NfsApiParamChecker.getNfsHostFromUrl(NFS_IPV4_URL) == IPV4 assert NfsApiParamChecker.getNfsPathFromUrl(NFS_IPV4_URL) == NFS_EXPORT_PATH From 83717cdec5f785e9bb97efce38ea3375777b5bc2 Mon Sep 17 00:00:00 2001 From: "shixin.ruan" Date: Fri, 5 Jun 2026 15:34:53 +0900 Subject: [PATCH 50/53] [mgt-ipv6]: isolate mn ip property tests Jira: ZSTAC-85711 Verify: - mvn -pl core -DskipTests compile Change-Id: Iecac893d032b76cf86218ca9cbbb0f72b8ecd9aa --- .../core/ManagementNetworkIpv6Case.groovy | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/test/src/test/groovy/org/zstack/test/integration/core/ManagementNetworkIpv6Case.groovy b/test/src/test/groovy/org/zstack/test/integration/core/ManagementNetworkIpv6Case.groovy index 414d9dd09b6..ccfd667082a 100644 --- a/test/src/test/groovy/org/zstack/test/integration/core/ManagementNetworkIpv6Case.groovy +++ b/test/src/test/groovy/org/zstack/test/integration/core/ManagementNetworkIpv6Case.groovy @@ -360,19 +360,27 @@ class ManagementNetworkIpv6Case extends SubCase { } private void withManagementServerIpProperties(Map properties, Closure closure) { + List managedKeys = [ + "management.server.ip", + "management.server.ip4", + "management.server.ip6", + ] Map oldValues = [:] - properties.keySet().each { key -> + managedKeys.each { key -> oldValues[key] = System.getProperty(key) } try { resetCachedManagementServerIp() + managedKeys.each { key -> + System.clearProperty(key) + } properties.each { key, value -> System.setProperty(key, value) } closure.call() } finally { - properties.keySet().each { key -> + managedKeys.each { key -> if (oldValues[key] == null) { System.clearProperty(key) } else { From c8ed4533532c1f6419e14b3504a0f0085054247d Mon Sep 17 00:00:00 2001 From: "shixin.ruan" Date: Sun, 7 Jun 2026 19:39:18 +0800 Subject: [PATCH 51/53] [install]: support IPv6 JDBC host Use bracketed IPv6 for Flyway JDBC URLs. Keep raw IPv6 for mysql CLI hosts. Resolves: ZSTAC-85731 Change-Id: I98c0b86622840f3e2a3780a62325113bf1ed9369 --- conf/deploydb.sh | 23 ++++++++++++++++------- conf/deployuidb.sh | 26 ++++++++++++++++++-------- 2 files changed, 34 insertions(+), 15 deletions(-) diff --git a/conf/deploydb.sh b/conf/deploydb.sh index 033da079954..c0d27e12d41 100755 --- a/conf/deploydb.sh +++ b/conf/deploydb.sh @@ -8,6 +8,16 @@ password="$2" host="$3" port="$4" zstack_user_password="$5" +mysql_host="$host" + +case "$mysql_host" in + \[*\]) mysql_host="${mysql_host#\[}"; mysql_host="${mysql_host%\]}" ;; +esac + +jdbc_host="$mysql_host" +case "$jdbc_host" in + *:*) jdbc_host="[$jdbc_host]" ;; +esac base=`dirname $0` @@ -30,7 +40,7 @@ if command -v greatdb &> /dev/null; then fi mysql_run() { - $MYSQL --user=$user --password=$password --host=$host --port=$port "$@" + $MYSQL --user=$user --password=$password --host=$mysql_host --port=$port "$@" } if command -v greatdb &> /dev/null; then @@ -67,7 +77,7 @@ mkdir -p $flyway_sql cp $base/db/V0.6__schema.sql $flyway_sql cp $base/db/upgrade/* $flyway_sql -url="jdbc:mysql://$host:$port/zstack" +url="jdbc:mysql://$jdbc_host:$port/zstack" bash $flyway -user=$user -password=$password -url=$url clean @@ -81,7 +91,7 @@ eval "rm -f $flyway_sql/*" cp $base/db/V0.6__schema_buildin_httpserver.sql $flyway_sql -url="jdbc:mysql://$host:$port/zstack_rest" +url="jdbc:mysql://$jdbc_host:$port/zstack_rest" bash $flyway -user=$user -password=$password -url=$url clean bash $flyway -user=$user -password=$password -url=$url migrate @@ -92,7 +102,7 @@ hostname=`hostname` [ -z $zstack_user_password ] && zstack_user_password='' if command -v greatdb &> /dev/null; then - $MYSQL --user=$user --password=$password --host=$host --port=$port << EOF + $MYSQL --user=$user --password=$password --host=$mysql_host --port=$port << EOF drop user if exists zstack; drop user if exists zstack_rest; create user if not exists 'zstack'@'localhost' identified by "$zstack_user_password"; @@ -110,7 +120,7 @@ EOF else db_version=`$MYSQL --version | awk '/Distrib/{print $5}' |awk -F'.' '{print $1}'` if [ $db_version -ge 10 ];then - $MYSQL --user=$user --password=$password --host=$host --port=$port << EOF + $MYSQL --user=$user --password=$password --host=$mysql_host --port=$port << EOF drop user if exists zstack; drop user if exists zstack_rest; create user 'zstack' identified by "$zstack_user_password"; @@ -122,7 +132,7 @@ else flush privileges; EOF else - $MYSQL --user=$user --password=$password --host=$host --port=$port << EOF + $MYSQL --user=$user --password=$password --host=$mysql_host --port=$port << EOF grant usage on *.* to 'zstack'@'localhost'; grant usage on *.* to 'zstack'@'%'; drop user zstack; @@ -137,4 +147,3 @@ EOF EOF fi fi - diff --git a/conf/deployuidb.sh b/conf/deployuidb.sh index 16a7e659576..cba144bdcad 100755 --- a/conf/deployuidb.sh +++ b/conf/deployuidb.sh @@ -6,6 +6,16 @@ password="$2" host="$3" port="$4" zstack_ui_db_password="$5" +mysql_host="$host" + +case "$mysql_host" in + \[*\]) mysql_host="${mysql_host#\[}"; mysql_host="${mysql_host%\]}" ;; +esac + +jdbc_host="$mysql_host" +case "$jdbc_host" in + *:*) jdbc_host="[$jdbc_host]" ;; +esac MYSQL='mysql' @@ -27,18 +37,18 @@ flyway_sql="$base/tools/flyway-3.2.1/sql/" # give grant option to the new management ip after `zstack-ctl change_ip` if command -v greatdb &> /dev/null; then $MYSQL --user=$user --password=$password --port=$port << EOF - grant all privileges on *.* to 'root'@"$host" with grant option; + grant all privileges on *.* to 'root'@"$mysql_host" with grant option; FLUSH PRIVILEGES; EOF else $MYSQL --user=$user --password=$password --port=$port << EOF - grant all privileges on *.* to 'root'@"$host" identified by "$password" with grant option; + grant all privileges on *.* to 'root'@"$mysql_host" identified by "$password" with grant option; FLUSH PRIVILEGES; EOF fi if command -v greatdb &> /dev/null; then - $MYSQL --user=$user --password=$password --host=$host --port=$port << EOF + $MYSQL --user=$user --password=$password --host=$mysql_host --port=$port << EOF grant usage on *.* to 'root'@'localhost'; grant usage on *.* to 'root'@'%'; DROP DATABASE IF EXISTS zstack_ui; @@ -50,7 +60,7 @@ if command -v greatdb &> /dev/null; then flush privileges; EOF else - $MYSQL --user=$user --password=$password --host=$host --port=$port << EOF + $MYSQL --user=$user --password=$password --host=$mysql_host --port=$port << EOF grant usage on *.* to 'root'@'localhost'; grant usage on *.* to 'root'@'%'; DROP DATABASE IF EXISTS zstack_ui; @@ -67,7 +77,7 @@ mkdir -p $flyway_sql ui_schema_path=`echo ~zstack`"/zstack-ui/tmp/WEB-INF/classes/db/migration/" if [ -d $ui_schema_path ]; then cp $ui_schema_path/* $flyway_sql - url="jdbc:mysql://$host:$port/zstack_ui" + url="jdbc:mysql://$jdbc_host:$port/zstack_ui" bash $flyway -user=$user -password=$password -url=$url clean bash $flyway -user=$user -password=$password -url=$url migrate eval "rm -f $flyway_sql/*" @@ -76,7 +86,7 @@ fi hostname=`hostname` if command -v greatdb &> /dev/null; then - $MYSQL --user=$user --password=$password --host=$host --port=$port << EOF + $MYSQL --user=$user --password=$password --host=$mysql_host --port=$port << EOF drop user if exists zstack_ui; create user 'zstack_ui' identified by "$zstack_ui_db_password"; create user if not exists 'zstack_ui'@'localhost' identified by "$zstack_ui_db_password"; @@ -88,7 +98,7 @@ EOF else db_version=`$MYSQL --version | awk '/Distrib/{print $5}' |awk -F'.' '{print $1}'` if [ $db_version -ge 10 ];then - $MYSQL --user=$user --password=$password --host=$host --port=$port << EOF + $MYSQL --user=$user --password=$password --host=$mysql_host --port=$port << EOF drop user if exists zstack_ui; create user 'zstack_ui' identified by "$zstack_ui_db_password"; grant all privileges on zstack_ui.* to zstack_ui@'localhost' identified by "$zstack_ui_db_password"; @@ -96,7 +106,7 @@ else flush privileges; EOF else - $MYSQL --user=$user --password=$password --host=$host --port=$port << EOF + $MYSQL --user=$user --password=$password --host=$mysql_host --port=$port << EOF grant usage on *.* to 'zstack_ui'@'localhost'; grant usage on *.* to 'zstack_ui'@'%'; drop user zstack_ui; From 53299235258134ef2e77318aee57dcc413172d91 Mon Sep 17 00:00:00 2001 From: "shixin.ruan" Date: Tue, 9 Jun 2026 18:58:38 +0900 Subject: [PATCH 52/53] [sdk]: add host interface prefix Expose prefixLength in SetIpOnHostNetworkInterfaceAction for IPv6 host interface configuration. Resolves: ZSTAC-85916 Change-Id: I982cf6d0c965bdffdd5b5e84f74a43355b986ce6 --- .../java/org/zstack/sdk/SetIpOnHostNetworkInterfaceAction.java | 3 +++ 1 file changed, 3 insertions(+) diff --git a/sdk/src/main/java/org/zstack/sdk/SetIpOnHostNetworkInterfaceAction.java b/sdk/src/main/java/org/zstack/sdk/SetIpOnHostNetworkInterfaceAction.java index 87fa11c3400..19d3cefc655 100644 --- a/sdk/src/main/java/org/zstack/sdk/SetIpOnHostNetworkInterfaceAction.java +++ b/sdk/src/main/java/org/zstack/sdk/SetIpOnHostNetworkInterfaceAction.java @@ -34,6 +34,9 @@ public Result throwExceptionIfError() { @Param(required = false, nonempty = false, nullElements = false, emptyString = true, noTrim = false) public java.lang.String netmask; + @Param(required = false, nonempty = false, nullElements = false, emptyString = true, numberRange = {0L,128L}, noTrim = false) + public java.lang.Integer prefixLength; + @Param(required = false) public java.util.List systemTags; From aae47305ab5e909c008873c6eb10904bd8dfab09 Mon Sep 17 00:00:00 2001 From: "shixin.ruan" Date: Wed, 10 Jun 2026 13:39:27 +0900 Subject: [PATCH 53/53] [vrouter]: skip ha vr demote on vf migration Use the post-migration syncVip flow for VF VR migration. Avoid double completion in preVmMigration. Resolves: ZSTAC-85916 Change-Id: Ifdd95ea1e165fd9cc388c6c22462cb3b5bbad599 --- .../VirtualRouterManagerImpl.java | 44 ------------------- 1 file changed, 44 deletions(-) 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 b3a48429692..d19f8531a16 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 @@ -2877,50 +2877,6 @@ private List findVipsOnVirtualRouter(List vfNics, String @Override public void preVmMigration(VmInstanceInventory vm, VmMigrationType type, String dstHostUuid, Completion completion) { - if (ApplianceVmConstant.APPLIANCE_VM_TYPE.equals(vm.getType())) { - VirtualRouterVmVO vrVo = Q.New(VirtualRouterVmVO.class).eq(VirtualRouterVmVO_.uuid, vm.getUuid()).find(); - if (vrVo == null) { - completion.success(); - return; - } - VirtualRouterVmInventory inv = VirtualRouterVmInventory.valueOf(vrVo); - List vfNics = inv.getVmNics().stream() - .filter(nic -> !Objects.equals(nic.getType(), VmInstanceConstant.VIRTUAL_NIC_TYPE)) - .collect(Collectors.toList()); - if (vrVo.isHaEnabled() && !vfNics.isEmpty()) { - List exps = pluginRgty.getExtensionList(VirtualRouterHaGroupExtensionPoint.class); - if (exps.isEmpty()) { - completion.success(); - return; - } - String peerUuid = exps.get(0).getPeerUuid(vrVo.getUuid()); - if (peerUuid == null) { - completion.success(); - return; - } - if (ApplianceVmHaStatus.Master.equals(vrVo.getHaStatus()) && - ApplianceVmStatus.Connected.equals(vrVo.getStatus()) && - Q.New(VirtualRouterVmVO.class).eq(VirtualRouterVmVO_.status, ApplianceVmStatus.Connected) - .eq(VirtualRouterVmVO_.uuid, peerUuid).isExists()) { - logger.debug(String.format("demote ha master before migrate, virtual router[uuid:%s]", vrVo.getUuid())); - VirtualRouterAsyncHttpCallMsg msg = new VirtualRouterAsyncHttpCallMsg(); - msg.setVmInstanceUuid(vrVo.getUuid()); - msg.setCommand(new VirtualRouterCommands.AgentCommand()); - msg.setCheckStatus(true); - msg.setPath(VirtualRouterConstant.VR_HA_MASTER_DEMOTE); - bus.makeTargetServiceIdByResourceUuid(msg, VmInstanceConstant.SERVICE_ID, vrVo.getUuid()); - bus.send(msg, new CloudBusCallBack(null) { - @Override - public void run(MessageReply reply) { - if (!reply.isSuccess()) { - logger.warn(reply.getError().toString()); - } - completion.success(); - } - }); - } - } - } completion.success(); } }