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);

Reply via email to