You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@cloudstack.apache.org by ha...@apache.org on 2026/08/17 07:45:07 UTC
(cloudstack) branch 4.20 updated: Don't allow adding of existing host to another zone, pod or cluster (#13182)
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 <87...@users.noreply.github.com>
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);