From aca40888f5094c9b3bd143efc13e2fd7a7543edd Mon Sep 17 00:00:00 2001 From: Sachin R <32716246+sachindoddaguni@users.noreply.github.com> Date: Tue, 4 Nov 2025 15:08:39 -0800 Subject: [PATCH] =?UTF-8?q?ConfigureStorageAccessCmd:=20Allow=20storage=20?= =?UTF-8?q?access=20configuration=20on=20empt=E2=80=A6=20(#578)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * ConfigureStorageAccessCmd: Allow storage access configuration on empty clusters/pods/zones * update as per comments Co-authored-by: mprokopchuk * update as per review comments Co-authored-by: mprokopchuk * update as per comments Co-authored-by: mprokopchuk --------- Co-authored-by: Sachin R Doddaguni Co-authored-by: mprokopchuk (cherry picked from commit 32736b9e262522912cf8755acabfb7400006252f) --- .../cloud/resource/ResourceManagerImpl.java | 9 +- .../resource/ResourceManagerImplTest.java | 92 +++++++++++++++++++ 2 files changed, 98 insertions(+), 3 deletions(-) diff --git a/server/src/main/java/com/cloud/resource/ResourceManagerImpl.java b/server/src/main/java/com/cloud/resource/ResourceManagerImpl.java index 87059badbec5..63951dc5983f 100755 --- a/server/src/main/java/com/cloud/resource/ResourceManagerImpl.java +++ b/server/src/main/java/com/cloud/resource/ResourceManagerImpl.java @@ -2465,7 +2465,8 @@ protected void checkIfAllHostsInUse(List sagsToDelete, Long clusterId, L List hostsInZone = _hostDao.findByDataCenterId(zoneId); Set hostIdsInUseSet = hostIdsUsingStorageAccessGroups.stream().collect(Collectors.toSet()); - boolean allInUseZone = hostsInZone.stream() + // allMatch returns true on empty stream, need to check whether collection is not empty first + boolean allInUseZone = !hostsInZone.isEmpty() && hostsInZone.stream() .map(HostVO::getId) .allMatch(hostIdsInUseSet::contains); @@ -2479,7 +2480,8 @@ protected void checkIfAllHostsInUse(List sagsToDelete, Long clusterId, L List hostsInCluster = _hostDao.findByClusterId(clusterId, Type.Routing); Set hostIdsInUseSet = hostIdsUsingStorageAccessGroups.stream().collect(Collectors.toSet()); - boolean allInUseCluster = hostsInCluster.stream() + // allMatch returns true on empty stream, need to check whether collection is not empty first + boolean allInUseCluster = !hostsInCluster.isEmpty() && hostsInCluster.stream() .map(HostVO::getId) .allMatch(hostIdsInUseSet::contains); @@ -2493,7 +2495,8 @@ protected void checkIfAllHostsInUse(List sagsToDelete, Long clusterId, L List hostsInPod = _hostDao.findByPodId(podId, Type.Routing); Set hostIdsInUseSet = hostIdsUsingStorageAccessGroups.stream().collect(Collectors.toSet()); - boolean allInUsePod = hostsInPod.stream() + // allMatch returns true on empty stream, need to check whether collection is not empty first + boolean allInUsePod = !hostsInPod.isEmpty() && hostsInPod.stream() .map(HostVO::getId) .allMatch(hostIdsInUseSet::contains); diff --git a/server/src/test/java/com/cloud/resource/ResourceManagerImplTest.java b/server/src/test/java/com/cloud/resource/ResourceManagerImplTest.java index 30a021591a5d..d644b036949a 100644 --- a/server/src/test/java/com/cloud/resource/ResourceManagerImplTest.java +++ b/server/src/test/java/com/cloud/resource/ResourceManagerImplTest.java @@ -1211,4 +1211,96 @@ public void executeUserRequestDefaultOverloadPassesFalseForDeleteHost() throws E Mockito.verify(resourceManager).doDeleteHost(hostId, false, false); } + + @Test + public void testUpdateClusterStorageAccessGroupsWithEmptyHostsInCluster() { + Long clusterId = 1L; + List newStorageAccessGroups = Arrays.asList("sag1", "sag2"); + + ClusterVO cluster = Mockito.mock(ClusterVO.class); + Mockito.when(cluster.getId()).thenReturn(clusterId); + Mockito.when(cluster.getStorageAccessGroups()).thenReturn("sag3,sag4"); // existing SAGs + Mockito.when(resourceManager.getCluster(clusterId)).thenReturn(cluster); + List emptyHostsList = new ArrayList<>(); + Mockito.when(hostDao.findHypervisorHostInCluster(clusterId)).thenReturn(emptyHostsList); + Mockito.when(hostDao.findByClusterId(clusterId, Host.Type.Routing)).thenReturn(emptyHostsList); + List emptyHostIdsList = new ArrayList<>(); + Mockito.doReturn(emptyHostIdsList).when(resourceManager) + .listOfHostIdsUsingTheStorageAccessGroups(Mockito.anyList(), eq(clusterId), eq(null), eq(null)); + try { + resourceManager.updateClusterStorageAccessGroups(clusterId, newStorageAccessGroups); + } catch (CloudRuntimeException e) { + Assert.fail("updateClusterStorageAccessGroups should not throw CloudRuntimeException when cluster has no hosts. Error: " + e.getMessage()); + } + Mockito.verify(resourceManager).checkIfAllHostsInUse(Mockito.anyList(), eq(clusterId), eq(null), eq(null)); + } + + @Test + public void testUpdateClusterStorageAccessGroupsWithEmptyHostsInZone() { + List sagsToDelete = Arrays.asList("tag1", "tag2"); + Long clusterId = null; + Long podId = null; + Long zoneId = 3L; + + List emptyHostIdsList = new ArrayList<>(); + Mockito.doReturn(emptyHostIdsList).when(resourceManager) + .listOfHostIdsUsingTheStorageAccessGroups(sagsToDelete, clusterId, podId, zoneId); + List emptyHostsInZone = new ArrayList<>(); + Mockito.doReturn(emptyHostsInZone).when(hostDao).findByDataCenterId(zoneId); + + try { + resourceManager.checkIfAllHostsInUse(sagsToDelete, clusterId, podId, zoneId); + } catch (CloudRuntimeException e) { + Assert.fail("checkIfAllHostsInUse should not throw CloudRuntimeException when zone has no hosts. Error: " + e.getMessage()); + } + Mockito.verify(resourceManager).checkIfAllHostsInUse(Mockito.anyList(), eq(null), eq(null), eq(zoneId)); + } + + @Test + public void testUpdateClusterStorageAccessGroupsWithEmptyHostsInPod() { + List sagsToDelete = Arrays.asList("tag1", "tag2"); + Long clusterId = null; + Long podId = 2L; + Long zoneId = null; + + List emptyHostIdsList = new ArrayList<>(); + Mockito.doReturn(emptyHostIdsList).when(resourceManager) + .listOfHostIdsUsingTheStorageAccessGroups(sagsToDelete, clusterId, podId, zoneId); + List emptyHostsInPod = new ArrayList<>(); + Mockito.doReturn(emptyHostsInPod).when(hostDao).findByPodId(podId, Host.Type.Routing); + + try { + resourceManager.checkIfAllHostsInUse(sagsToDelete, clusterId, podId, zoneId); + } catch (CloudRuntimeException e) { + Assert.fail("checkIfAllHostsInUse should not throw CloudRuntimeException when pod has no hosts. Error: " + e.getMessage()); + } + Mockito.verify(resourceManager).checkIfAllHostsInUse(Mockito.anyList(), eq(null), eq(podId), eq(null)); + } + + @Test + public void testCheckIfAllHostsInUseWithEmptyHostsInMultipleLevels() { + List sagsToDelete = Arrays.asList("tag1", "tag2"); + Long clusterId = 1L; + Long podId = 2L; + Long zoneId = 3L; + + List emptyHostIdsList = new ArrayList<>(); + Mockito.doReturn(emptyHostIdsList).when(resourceManager) + .listOfHostIdsUsingTheStorageAccessGroups(sagsToDelete, clusterId, podId, zoneId); + List emptyHostsInZone = new ArrayList<>(); + List emptyHostsInCluster = new ArrayList<>(); + List emptyHostsInPod = new ArrayList<>(); + Mockito.doReturn(emptyHostsInZone).when(hostDao).findByDataCenterId(zoneId); + Mockito.doReturn(emptyHostsInCluster).when(hostDao).findByClusterId(clusterId, Host.Type.Routing); + Mockito.doReturn(emptyHostsInPod).when(hostDao).findByPodId(podId, Host.Type.Routing); + + try { + resourceManager.checkIfAllHostsInUse(sagsToDelete, clusterId, podId, zoneId); + } catch (CloudRuntimeException e) { + Assert.fail("checkIfAllHostsInUse should not throw CloudRuntimeException when all levels have no hosts. Error: " + e.getMessage()); + } + Mockito.verify(hostDao).findByDataCenterId(zoneId); + Mockito.verify(hostDao).findByClusterId(clusterId, Host.Type.Routing); + Mockito.verify(hostDao).findByPodId(podId, Host.Type.Routing); + } }