From b13e29993202fa40003b769058ee4f69c363073c Mon Sep 17 00:00:00 2001 From: "shixin.ruan" Date: Wed, 27 May 2026 12:34:59 +0900 Subject: [PATCH 01/14] [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 02/14] [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 03/14] [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 04/14] [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 05/14] [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 06/14] [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 07/14] [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 08/14] [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 09/14] [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 123bd2bd9a003234ccc878bff59cb6cbd2669a8d Mon Sep 17 00:00:00 2001 From: "shixin.ruan" Date: Fri, 29 May 2026 20:57:01 +0900 Subject: [PATCH 10/14] [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 11/14] [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 12/14] [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 13/14] [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 14/14] [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";