This is an automated email from the ASF dual-hosted git repository.
harikrishna-patnala pushed a commit to branch 4.20
in repository https://gitbox.apache.org/repos/asf/cloudstack.git
The following commit(s) were added to refs/heads/4.20 by this push:
new 2bc8d299733 Don't allow adding of existing host to another zone, pod
or cluster (#13182)
2bc8d299733 is described below
commit 2bc8d29973366f0555d779afd83f6ed2bc538a84
Author: Vishesh <[email protected]>
AuthorDate: Mon Aug 17 13:15:00 2026 +0530
Don't allow adding of existing host to another zone, pod or cluster (#13182)
---
.../com/cloud/resource/ResourceManagerImpl.java | 55 +++++++++++++
.../cloud/resource/ResourceManagerImplTest.java | 90 ++++++++++++++++++++++
2 files changed, 145 insertions(+)
diff --git a/server/src/main/java/com/cloud/resource/ResourceManagerImpl.java
b/server/src/main/java/com/cloud/resource/ResourceManagerImpl.java
index 6e78e3bea83..a6a75fc3961 100755
--- a/server/src/main/java/com/cloud/resource/ResourceManagerImpl.java
+++ b/server/src/main/java/com/cloud/resource/ResourceManagerImpl.java
@@ -19,9 +19,11 @@ package com.cloud.resource;
import static
com.cloud.configuration.ConfigurationManagerImpl.MIGRATE_VM_ACROSS_CLUSTERS;
import static
com.cloud.configuration.ConfigurationManagerImpl.SET_HOST_DOWN_TO_MAINTENANCE;
+import java.net.InetAddress;
import java.net.URI;
import java.net.URISyntaxException;
import java.net.URLDecoder;
+import java.net.UnknownHostException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
@@ -31,6 +33,7 @@ import java.util.Iterator;
import java.util.List;
import java.util.Locale;
import java.util.Map;
+import java.util.Objects;
import java.util.Random;
import java.util.stream.Collectors;
@@ -646,6 +649,7 @@ public class ResourceManagerImpl extends ManagerBase
implements ResourceManager,
String hypervisorType =
cmd.getHypervisor().equalsIgnoreCase(HypervisorGuru.HypervisorCustomDisplayName.value())
?
"Custom" : cmd.getHypervisor();
+ checkForDuplicateHost(url);
return discoverHostsFull(dcId, podId, clusterId, clusterName, url,
username, password, hypervisorType, hostTags, cmd.getFullUrlParams(), false);
}
@@ -2281,6 +2285,29 @@ public class ResourceManagerImpl extends ManagerBase
implements ResourceManager,
return null;
}
+ protected void validateExistingHostLocationImmutable(final HostVO host,
final boolean newHost,
+ final long dcId, final Long podId, final Long clusterId, final
StartupCommand startup) {
+ if (newHost || host == null || host.getType() != Host.Type.Routing) {
+ return;
+ }
+ final long existingDcId = host.getDataCenterId();
+ final Long existingPodId = host.getPodId();
+ final Long existingClusterId = host.getClusterId();
+ if (existingPodId == null || existingClusterId == null) {
+ return;
+ }
+ if (existingDcId == dcId && Objects.equals(existingPodId, podId) &&
Objects.equals(existingClusterId, clusterId)) {
+ return;
+ }
+ final String identity = Objects.toString(host.getUuid(),
host.getGuid());
+ final String ip = startup != null ? startup.getPrivateIpAddress() :
"unknown";
+ throw new InvalidParameterValueException(
+ String.format("Host %s (ip: %s) is already registered in
[zone: %s, pod: %s, cluster: %s] and cannot " +
+ "be re-added or reconnected with [zone: %s,
pod: %s, cluster: %s]. Zone, pod and " +
+ "cluster of an existing host are immutable.",
+ identity, ip, existingDcId, existingPodId,
existingClusterId, dcId, podId, clusterId));
+ }
+
protected HostVO createHostVO(final StartupCommand[] cmds, final
ServerResource resource, final Map<String, String> details, List<String>
hostTags,
final ResourceStateAdapter.Event stateEvent) {
boolean newHost = false;
@@ -2356,6 +2383,8 @@ public class ResourceManagerImpl extends ManagerBase
implements ResourceManager,
}
}
+ validateExistingHostLocationImmutable(host, newHost, dcId, podId,
clusterId, startup);
+
host.setDataCenterId(dc.getId());
host.setPodId(podId);
host.setClusterId(clusterId);
@@ -2585,6 +2614,32 @@ public class ResourceManagerImpl extends ManagerBase
implements ResourceManager,
return host;
}
+ void checkForDuplicateHost(final String url) {
+ String hostIpOrName = null;
+ String ipAddress = null;
+ try {
+ hostIpOrName = new URI(UriUtils.encodeURIComponent(url)).getHost();
+ if (StringUtils.isBlank(hostIpOrName)) {
+ return;
+ }
+ InetAddress ip = InetAddress.getByName(hostIpOrName);
+ ipAddress = ip.getHostAddress();
+ } catch (final URISyntaxException | UnknownHostException ignore) {
+ // unparseable URL or unknown host - discoverer will reject it
shortly anyway
+ return;
+ }
+
+ if (StringUtils.isNotBlank(ipAddress)) {
+ final HostVO existingByIp = _hostDao.findByIp(ipAddress);
+ // findByIp matches hosts of any type; only a Routing host is a
duplicate for addHost
+ if (existingByIp != null &&
Host.Type.Routing.equals(existingByIp.getType())) {
+ throw new InvalidParameterValueException(String.format(
+ "A host with IP address '%s' (%s) already exists (id:
%s). Remove it before adding again.",
+ ipAddress, hostIpOrName, existingByIp.getUuid()));
+ }
+ }
+ }
+
private Host createHostAndAgentDeferred(final ServerResource resource,
final Map<String, String> details, final boolean old, final List<String>
hostTags, final boolean forRebalance) {
HostVO host = null;
StartupCommand[] cmds = null;
diff --git
a/server/src/test/java/com/cloud/resource/ResourceManagerImplTest.java
b/server/src/test/java/com/cloud/resource/ResourceManagerImplTest.java
index 91e4bf7a47b..a28d3aaeac0 100644
--- a/server/src/test/java/com/cloud/resource/ResourceManagerImplTest.java
+++ b/server/src/test/java/com/cloud/resource/ResourceManagerImplTest.java
@@ -18,6 +18,7 @@
package com.cloud.resource;
import com.cloud.agent.AgentManager;
+import com.cloud.agent.api.StartupCommand;
import com.cloud.agent.api.GetVncPortAnswer;
import com.cloud.agent.api.GetVncPortCommand;
import com.cloud.capacity.dao.CapacityDao;
@@ -218,6 +219,27 @@ public class ResourceManagerImplTest {
closeable.close();
}
+ @Test(expected = InvalidParameterValueException.class)
+ public void testCheckForDuplicateHostThrowsWhenIpAlreadyExists() {
+ when(hostDao.findByIp("10.0.0.10")).thenReturn(host);
+ resourceManager.checkForDuplicateHost("http://10.0.0.10");
+ }
+
+ @Test
+ public void testCheckForDuplicateHostAllowsUniqueHost() {
+ when(hostDao.findByIp("10.0.0.30")).thenReturn(null);
+ resourceManager.checkForDuplicateHost("http://10.0.0.30");
+ verify(hostDao, times(1)).findByIp("10.0.0.30");
+ }
+
+ @Test
+ public void testCheckForDuplicateHostIgnoresNonRoutingHost() {
+ when(host.getType()).thenReturn(Host.Type.SecondaryStorage);
+ when(hostDao.findByIp("10.0.0.20")).thenReturn(host);
+
resourceManager.checkForDuplicateHost("nfs://10.0.0.20/export/secondary");
+ verify(hostDao, times(1)).findByIp("10.0.0.20");
+ }
+
@Test
public void testCheckAndMaintainEnterMaintenanceModeNoVms() throws
NoTransitionException {
// Test entering into maintenance with no VMs running on host.
@@ -335,6 +357,74 @@ public class ResourceManagerImplTest {
resourceManager.getHostCredentials(host);
}
+ private HostVO mockExistingRoutingHost(long dcId, Long podId, Long
clusterId) {
+ HostVO existing = Mockito.mock(HostVO.class);
+ when(existing.getType()).thenReturn(Host.Type.Routing);
+ when(existing.getDataCenterId()).thenReturn(dcId);
+ when(existing.getPodId()).thenReturn(podId);
+ when(existing.getClusterId()).thenReturn(clusterId);
+ when(existing.getUuid()).thenReturn("host-uuid");
+ return existing;
+ }
+
+ @Test(expected = InvalidParameterValueException.class)
+ public void testValidateExistingHostLocationImmutableRejectsZoneChange() {
+ HostVO existing = mockExistingRoutingHost(1L, 10L, 100L);
+ StartupCommand startup = Mockito.mock(StartupCommand.class);
+ when(startup.getPrivateIpAddress()).thenReturn("10.10.10.10");
+ resourceManager.validateExistingHostLocationImmutable(existing, false,
2L, 10L, 100L, startup);
+ }
+
+ @Test(expected = InvalidParameterValueException.class)
+ public void testValidateExistingHostLocationImmutableRejectsPodChange() {
+ HostVO existing = mockExistingRoutingHost(1L, 10L, 100L);
+ StartupCommand startup = Mockito.mock(StartupCommand.class);
+ when(startup.getPrivateIpAddress()).thenReturn("10.10.10.10");
+ resourceManager.validateExistingHostLocationImmutable(existing, false,
1L, 11L, 100L, startup);
+ }
+
+ @Test(expected = InvalidParameterValueException.class)
+ public void
testValidateExistingHostLocationImmutableRejectsClusterChange() {
+ HostVO existing = mockExistingRoutingHost(1L, 10L, 100L);
+ StartupCommand startup = Mockito.mock(StartupCommand.class);
+ when(startup.getPrivateIpAddress()).thenReturn("10.10.10.10");
+ resourceManager.validateExistingHostLocationImmutable(existing, false,
1L, 10L, 101L, startup);
+ }
+
+ @Test
+ public void
testValidateExistingHostLocationImmutableAllowsSameTupleReconnect() {
+ HostVO existing = mockExistingRoutingHost(1L, 10L, 100L);
+ resourceManager.validateExistingHostLocationImmutable(existing, false,
1L, 10L, 100L, null);
+ }
+
+ @Test
+ public void testValidateExistingHostLocationImmutableAllowsNewHost() {
+ HostVO existing = mockExistingRoutingHost(2L, 20L, 200L);
+ resourceManager.validateExistingHostLocationImmutable(existing, true,
1L, 10L, 100L, null);
+ }
+
+ @Test
+ public void testValidateExistingHostLocationImmutableSkipsNonRoutingHost()
{
+ HostVO existing = Mockito.mock(HostVO.class);
+ when(existing.getType()).thenReturn(Host.Type.SecondaryStorageVM);
+ resourceManager.validateExistingHostLocationImmutable(existing, false,
1L, 10L, 100L, null);
+ }
+
+ @Test
+ public void
testValidateExistingHostLocationImmutableSkipsPartialLocationRow() {
+ HostVO existing = Mockito.mock(HostVO.class);
+ when(existing.getType()).thenReturn(Host.Type.Routing);
+ when(existing.getDataCenterId()).thenReturn(1L);
+ when(existing.getPodId()).thenReturn(null);
+ when(existing.getClusterId()).thenReturn(null);
+ resourceManager.validateExistingHostLocationImmutable(existing, false,
2L, 10L, 100L, null);
+ }
+
+ @Test
+ public void
testValidateExistingHostLocationImmutableSkipsNullExistingHost() {
+ resourceManager.validateExistingHostLocationImmutable(null, false, 2L,
10L, 100L, null);
+ }
+
@Test
public void testGetHostCredentials() {
Ternary<String, String, String> credentials =
resourceManager.getHostCredentials(host);