ssvm: prevent sql constraint violation then a SVM was removed during network oiutage
diff --git a/engine/schema/src/main/java/com/cloud/host/HostVO.java b/engine/schema/src/main/java/com/cloud/host/HostVO.java index a449eb4..ae51be8 100644 --- a/engine/schema/src/main/java/com/cloud/host/HostVO.java +++ b/engine/schema/src/main/java/com/cloud/host/HostVO.java
@@ -600,6 +600,10 @@ return removed; } + public void setRemoved(Date removed) { + this.removed = removed; + } + @Override public String getVersion() { return version;
diff --git a/engine/schema/src/main/java/com/cloud/host/dao/HostDao.java b/engine/schema/src/main/java/com/cloud/host/dao/HostDao.java index 57ac35d..47019a7 100644 --- a/engine/schema/src/main/java/com/cloud/host/dao/HostDao.java +++ b/engine/schema/src/main/java/com/cloud/host/dao/HostDao.java
@@ -79,6 +79,8 @@ HostVO findByGuid(String guid); + HostVO findByGuidIncludingRemoved(String guid); + HostVO findByTypeNameAndZoneId(long zoneId, String name, Host.Type type); List<HostVO> findHypervisorHostInCluster(long clusterId);
diff --git a/engine/schema/src/main/java/com/cloud/host/dao/HostDaoImpl.java b/engine/schema/src/main/java/com/cloud/host/dao/HostDaoImpl.java index 8c3604d..221c4b1 100644 --- a/engine/schema/src/main/java/com/cloud/host/dao/HostDaoImpl.java +++ b/engine/schema/src/main/java/com/cloud/host/dao/HostDaoImpl.java
@@ -609,6 +609,12 @@ return findOneBy(sc); } + @Override + public HostVO findByGuidIncludingRemoved(String guid) { + SearchCriteria<HostVO> sc = GuidSearch.create("guid", guid); + return findOneIncludingRemovedBy(sc); + } + /* * Find hosts which is in Disconnected, Down, Alert and ping timeout and server is not null, set server to null */
diff --git a/server/src/main/java/com/cloud/resource/ResourceManagerImpl.java b/server/src/main/java/com/cloud/resource/ResourceManagerImpl.java index a6a75fc..67af99a 100755 --- a/server/src/main/java/com/cloud/resource/ResourceManagerImpl.java +++ b/server/src/main/java/com/cloud/resource/ResourceManagerImpl.java
@@ -2281,10 +2281,32 @@ return host; } + host = reviveRemovedHostByGuid(fullGuid); + if (host != null) { + return host; + } + logger.debug(String.format("Could not find Host by guid %s", fullGuid)); return null; } + /** + * A host can be marked removed (e.g. a system VM force-stopped and destroyed from the UI) while the + * underlying resource is still alive and later reconnects with the same guid. The `guid` column is + * uniquely constrained regardless of removal, so blindly inserting a new row for it fails with a + * SQLIntegrityConstraintViolationException. Revive the removed row instead of creating a duplicate. + */ + private HostVO reviveRemovedHostByGuid(String guid) { + HostVO removedHost = _hostDao.findByGuidIncludingRemoved(guid); + if (removedHost == null || removedHost.getRemoved() == null) { + return null; + } + logger.warn(String.format("Host with guid %s reconnected but was previously removed (id: %s); reviving the removed host record instead of creating a duplicate", + guid, removedHost.getId())); + removedHost.setRemoved(null); + return removedHost; + } + 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) {
diff --git a/server/src/test/java/com/cloud/resource/ResourceManagerImplTest.java b/server/src/test/java/com/cloud/resource/ResourceManagerImplTest.java index a28d3aa..c50cfb5 100644 --- a/server/src/test/java/com/cloud/resource/ResourceManagerImplTest.java +++ b/server/src/test/java/com/cloud/resource/ResourceManagerImplTest.java
@@ -64,9 +64,11 @@ import org.mockito.junit.MockitoJUnitRunner; import java.lang.reflect.Field; +import java.lang.reflect.Method; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; +import java.util.Date; import java.util.List; import java.util.UUID; @@ -683,4 +685,49 @@ resourceManager.destroyLocalStoragePoolVolumes(poolId); verify(volumeDao, never()).updateAndRemoveVolume(any(VolumeVO.class)); } + + private HostVO invokeGetNewHost(StartupCommand[] cmds) throws Exception { + Method method = ResourceManagerImpl.class.getDeclaredMethod("getNewHost", StartupCommand[].class); + method.setAccessible(true); + return (HostVO) method.invoke(resourceManager, (Object) cmds); + } + + @Test + public void testGetNewHostRevivesRemovedHostOnGuidCollision() throws Exception { + String guid = "s-14-VM-PremiumSecondaryStorageResource"; + String guidPrefix = "s-14-VM"; + StartupCommand cmd = Mockito.mock(StartupCommand.class); + when(cmd.getGuid()).thenReturn(guid); + when(cmd.getGuidWithoutResource()).thenReturn(guidPrefix); + + Mockito.doReturn(null).when(resourceManager).findHostByGuid(guid); + Mockito.doReturn(null).when(resourceManager).findHostByGuidPrefix(guidPrefix); + + HostVO removedHost = Mockito.mock(HostVO.class); + when(removedHost.getId()).thenReturn(99L); + when(removedHost.getRemoved()).thenReturn(new Date()); + when(hostDao.findByGuidIncludingRemoved(guid)).thenReturn(removedHost); + + HostVO result = invokeGetNewHost(new StartupCommand[]{cmd}); + + Assert.assertSame(removedHost, result); + Mockito.verify(removedHost).setRemoved(null); + } + + @Test + public void testGetNewHostReturnsNullWhenNoHostAndNoRemovedMatch() throws Exception { + String guid = "unknown-guid"; + String guidPrefix = "unknown"; + StartupCommand cmd = Mockito.mock(StartupCommand.class); + when(cmd.getGuid()).thenReturn(guid); + when(cmd.getGuidWithoutResource()).thenReturn(guidPrefix); + + Mockito.doReturn(null).when(resourceManager).findHostByGuid(guid); + Mockito.doReturn(null).when(resourceManager).findHostByGuidPrefix(guidPrefix); + when(hostDao.findByGuidIncludingRemoved(guid)).thenReturn(null); + + HostVO result = invokeGetNewHost(new StartupCommand[]{cmd}); + + Assert.assertNull(result); + } }