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