From fdb4eba6eaabea74caf54f46b7e9831f452f5d74 Mon Sep 17 00:00:00 2001 From: Abhishek Kumar Date: Tue, 7 Oct 2025 12:00:33 +0530 Subject: [PATCH] framework-cluster: use bind.interface for cluster communication Fixes #11460 Signed-off-by: Abhishek Kumar --- .../cluster/ClusterServiceServletImpl.java | 20 ++++++ .../ClusterServiceServletImplTest.java | 39 +++++++++++ .../utils/ServerPropertiesUtil.java | 66 +++++++++++++++++++ 3 files changed, 125 insertions(+) create mode 100644 utils/src/main/java/org/apache/cloudstack/utils/ServerPropertiesUtil.java diff --git a/framework/cluster/src/main/java/com/cloud/cluster/ClusterServiceServletImpl.java b/framework/cluster/src/main/java/com/cloud/cluster/ClusterServiceServletImpl.java index d582538c31e0..e76979f15122 100644 --- a/framework/cluster/src/main/java/com/cloud/cluster/ClusterServiceServletImpl.java +++ b/framework/cluster/src/main/java/com/cloud/cluster/ClusterServiceServletImpl.java @@ -18,6 +18,8 @@ import java.io.IOException; import java.io.UnsupportedEncodingException; +import java.net.InetAddress; +import java.net.UnknownHostException; import java.rmi.RemoteException; import java.security.GeneralSecurityException; import java.util.ArrayList; @@ -26,6 +28,7 @@ import javax.net.ssl.SSLContext; import org.apache.cloudstack.framework.ca.CAService; +import org.apache.cloudstack.utils.ServerPropertiesUtil; import org.apache.commons.httpclient.HttpStatus; import org.apache.http.NameValuePair; import org.apache.http.client.config.RequestConfig; @@ -41,6 +44,7 @@ import com.cloud.utils.HttpUtils; import com.cloud.utils.Profiler; +import com.cloud.utils.StringUtils; import com.cloud.utils.nio.Link; import com.google.gson.Gson; @@ -162,6 +166,20 @@ private String executePostMethod(final CloseableHttpClient client, final HttpPos return result; } + protected InetAddress getBindAddressIfAvailable() { + String bindAddressStr = ServerPropertiesUtil.getProperty("bind.interface"); + InetAddress bindAddress = null; + try { + if (StringUtils.isNotBlank(bindAddressStr)) { + bindAddress = InetAddress.getByName(bindAddressStr); + } + } catch (UnknownHostException e) { + logger.error("Unable to resolve bind address: {}", bindAddressStr, e); + throw new RuntimeException(e); + } + return bindAddress; + } + private CloseableHttpClient getHttpClient() { if (s_client == null) { SSLContext sslContext = null; @@ -172,7 +190,9 @@ private CloseableHttpClient getHttpClient() { } int timeout = ClusterServiceAdapter.ClusterMessageTimeOut.value() * 1000; + InetAddress bindAddress = getBindAddressIfAvailable(); RequestConfig config = RequestConfig.custom() + .setLocalAddress(bindAddress) .setConnectTimeout(timeout) .setConnectionRequestTimeout(timeout) .setSocketTimeout(timeout).build(); diff --git a/framework/cluster/src/test/java/com/cloud/cluster/ClusterServiceServletImplTest.java b/framework/cluster/src/test/java/com/cloud/cluster/ClusterServiceServletImplTest.java index 361c77dbeff4..cc5df3e11366 100644 --- a/framework/cluster/src/test/java/com/cloud/cluster/ClusterServiceServletImplTest.java +++ b/framework/cluster/src/test/java/com/cloud/cluster/ClusterServiceServletImplTest.java @@ -17,15 +17,18 @@ package com.cloud.cluster; +import java.net.InetAddress; import java.util.List; import java.util.Optional; +import org.apache.cloudstack.utils.ServerPropertiesUtil; import org.apache.commons.collections.CollectionUtils; import org.apache.http.NameValuePair; import org.junit.Assert; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.InjectMocks; +import org.mockito.MockedStatic; import org.mockito.Mockito; import org.mockito.junit.MockitoJUnitRunner; @@ -61,4 +64,40 @@ public void testPingPostParameters() { val = opt.get(); Assert.assertEquals(peer, val.getValue()); } + + @Test + public void getBindAddressIfAvailable_returnsInetAddress_whenBindAddressIsValid() { + try (MockedStatic ignored = Mockito.mockStatic(ServerPropertiesUtil.class)) { + Mockito.when(ServerPropertiesUtil.getProperty("bind.interface")).thenReturn("127.0.0.1"); + + InetAddress result = clusterServiceServlet.getBindAddressIfAvailable(); + + Assert.assertNotNull(result); + Assert.assertEquals("127.0.0.1", result.getHostAddress()); + } catch (RuntimeException e) { + Assert.fail("Unexpected RuntimeException: " + e.getMessage()); + } + } + + @Test + public void getBindAddressIfAvailable_returnsNull_whenBindAddressIsBlank() { + try (MockedStatic ignored = Mockito.mockStatic(ServerPropertiesUtil.class)) { + Mockito.when(ServerPropertiesUtil.getProperty("bind.interface")).thenReturn(""); + + InetAddress result = clusterServiceServlet.getBindAddressIfAvailable(); + + Assert.assertNull(result); + } catch (RuntimeException e) { + Assert.fail("Unexpected RuntimeException: " + e.getMessage()); + } + } + + @Test(expected = RuntimeException.class) + public void getBindAddressIfAvailable_throwsRuntimeException_whenBindAddressIsInvalid() throws RuntimeException { + try (MockedStatic ignored = Mockito.mockStatic(ServerPropertiesUtil.class)) { + Mockito.when(ServerPropertiesUtil.getProperty("bind.interface")).thenReturn("invalid-address"); + + clusterServiceServlet.getBindAddressIfAvailable(); + } + } } diff --git a/utils/src/main/java/org/apache/cloudstack/utils/ServerPropertiesUtil.java b/utils/src/main/java/org/apache/cloudstack/utils/ServerPropertiesUtil.java new file mode 100644 index 000000000000..a1116d00b464 --- /dev/null +++ b/utils/src/main/java/org/apache/cloudstack/utils/ServerPropertiesUtil.java @@ -0,0 +1,66 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +package org.apache.cloudstack.utils; + +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; +import java.util.Properties; +import java.util.concurrent.atomic.AtomicReference; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.cloud.utils.PropertiesUtil; + +public class ServerPropertiesUtil { + private static final Logger logger = LoggerFactory.getLogger(ServerPropertiesUtil.class); + private static final String PROPERTIES_FILE = "server.properties"; + private static final AtomicReference propertiesRef = new AtomicReference<>(); + + public static String getProperty(String name) { + Properties props = propertiesRef.get(); + if (props != null) { + return props.getProperty(name); + } + File propsFile = PropertiesUtil.findConfigFile(PROPERTIES_FILE); + if (propsFile == null) { + logger.error("{} file not found", PROPERTIES_FILE); + return null; + } + Properties tempProps = new Properties(); + try (FileInputStream is = new FileInputStream(propsFile)) { + tempProps.load(is); + } catch (IOException e) { + logger.error("Error loading {}: {}", PROPERTIES_FILE, e.getMessage(), e); + return null; + } + if (!propertiesRef.compareAndSet(null, tempProps)) { + tempProps = propertiesRef.get(); + } + return tempProps.getProperty(name); + } + + public static String getProperty(String name, String defaultValue) { + String value = getProperty(name); + if (value == null) { + value = defaultValue; + } + return value; + } +}