diff --git a/PendingReleaseNotes b/PendingReleaseNotes index 9670b6e7c13a..4431849e43f6 100644 --- a/PendingReleaseNotes +++ b/PendingReleaseNotes @@ -39,3 +39,8 @@ example.ver.1 > example.ver.2: which can now be attached to Instances. This is to prevent the Secondary Storage to grow to enormous sizes as Linux Distributions keep growing in size while a stripped down Linux should fit on a 2.88MB floppy. + +KVM console: + * Added the global setting `consoleproxy.kvm.multiple.viewers.enabled` to allow + multiple users to simultaneously access the console of the same KVM user VM. + The setting defaults to false to preserve the existing single-viewer behavior. diff --git a/api/src/main/java/org/apache/cloudstack/consoleproxy/ConsoleAccessManager.java b/api/src/main/java/org/apache/cloudstack/consoleproxy/ConsoleAccessManager.java index 655b8faf443a..4bdd2cc5edd7 100644 --- a/api/src/main/java/org/apache/cloudstack/consoleproxy/ConsoleAccessManager.java +++ b/api/src/main/java/org/apache/cloudstack/consoleproxy/ConsoleAccessManager.java @@ -41,6 +41,13 @@ public interface ConsoleAccessManager extends Manager, Configurable { false, ConfigKey.Scope.Global); + ConfigKey KvmMultipleConsoleViewersEnabled = new ConfigKey<>("Advanced", Boolean.class, + "consoleproxy.kvm.multiple.viewers.enabled", + "false", + "Determines whether multiple simultaneous console viewers are allowed for the same KVM user VM", + true, + ConfigKey.Scope.Global); + ConsoleEndpoint generateConsoleEndpoint(Long vmId, String extraSecurityToken, String clientAddress); boolean isSessionAllowed(String sessionUuid); diff --git a/server/src/main/java/org/apache/cloudstack/consoleproxy/ConsoleAccessManagerImpl.java b/server/src/main/java/org/apache/cloudstack/consoleproxy/ConsoleAccessManagerImpl.java index 237135b5176b..237f83053acc 100644 --- a/server/src/main/java/org/apache/cloudstack/consoleproxy/ConsoleAccessManagerImpl.java +++ b/server/src/main/java/org/apache/cloudstack/consoleproxy/ConsoleAccessManagerImpl.java @@ -160,7 +160,8 @@ public String getConfigComponentName() { public ConfigKey[] getConfigKeys() { return new ConfigKey[] { ConsoleAccessManager.ConsoleSessionCleanupInterval, - ConsoleAccessManager.ConsoleSessionCleanupRetentionHours + ConsoleAccessManager.ConsoleSessionCleanupRetentionHours, + ConsoleAccessManager.KvmMultipleConsoleViewersEnabled }; } @@ -474,6 +475,10 @@ protected Pair getHostAndPortForKVMMaintenanceHostIfNeeded(Host return null; } + protected boolean isKvmMultipleConsoleViewersEnabled() { + return ConsoleAccessManager.KvmMultipleConsoleViewersEnabled.value(); + } + protected ConsoleConnectionDetails getConsoleConnectionDetails(VirtualMachine vm, HostVO host) { String locale = null; String tag = vm.getUuid(); @@ -507,6 +512,13 @@ protected ConsoleConnectionDetails getConsoleConnectionDetails(VirtualMachine vm .getValue())); logger.debug("HyperV RDP port for {} on {} is: {}", vm, host, details.getPort()); } + + if (Hypervisor.HypervisorType.KVM.equals(host.getHypervisorType()) && + VirtualMachine.Type.User.equals(vm.getType()) && + isKvmMultipleConsoleViewersEnabled()) { + details.setSessionRequiresNewViewer(true); + } + return details; } diff --git a/server/src/test/java/org/apache/cloudstack/consoleproxy/ConsoleAccessManagerImplTest.java b/server/src/test/java/org/apache/cloudstack/consoleproxy/ConsoleAccessManagerImplTest.java index 97e6295da1a5..5d9f5bd8987a 100644 --- a/server/src/test/java/org/apache/cloudstack/consoleproxy/ConsoleAccessManagerImplTest.java +++ b/server/src/test/java/org/apache/cloudstack/consoleproxy/ConsoleAccessManagerImplTest.java @@ -545,6 +545,80 @@ public void getConsoleConnectionDetailsReturnsDetailsForKVMHypervisor() { Assert.assertEquals(port, result.getPort()); } + @Test + public void getConsoleConnectionDetailsRequiresNewViewerForKVMUserVmWhenMultipleViewersEnabled() { + VirtualMachine vm = Mockito.mock(VirtualMachine.class); + HostVO host = Mockito.mock(HostVO.class); + String hostAddress = "192.168.1.100"; + int port = 5900; + Pair hostPortInfo = new Pair<>(hostAddress, port); + + Mockito.when(vm.getUuid()).thenReturn("vm-uuid"); + Mockito.when(vm.getHostName()).thenReturn("vm-hostname"); + Mockito.when(vm.getVncPassword()).thenReturn("vnc-password"); + Mockito.when(vm.getType()).thenReturn(VirtualMachine.Type.User); + Mockito.when(host.getHypervisorType()).thenReturn(Hypervisor.HypervisorType.KVM); + Mockito.when(vmInstanceDetailsDao.listDetailsKeyPairs(Mockito.anyLong(), Mockito.anyList())).thenReturn(Map.of()); + Mockito.when(managementServer.getVncPort(vm)).thenReturn(hostPortInfo); + Mockito.doReturn(new Ternary<>(hostAddress, null, null)) + .when(consoleAccessManager).parseHostInfo(Mockito.anyString()); + Mockito.doReturn(true).when(consoleAccessManager).isKvmMultipleConsoleViewersEnabled(); + + ConsoleConnectionDetails result = consoleAccessManager.getConsoleConnectionDetails(vm, host); + + Assert.assertNotNull(result); + Assert.assertTrue(result.isSessionRequiresNewViewer()); + } + + @Test + public void getConsoleConnectionDetailsDoesNotRequireNewViewerForKVMUserVmWhenMultipleViewersDisabled() { + VirtualMachine vm = Mockito.mock(VirtualMachine.class); + HostVO host = Mockito.mock(HostVO.class); + String hostAddress = "192.168.1.100"; + int port = 5900; + Pair hostPortInfo = new Pair<>(hostAddress, port); + + Mockito.when(vm.getUuid()).thenReturn("vm-uuid"); + Mockito.when(vm.getHostName()).thenReturn("vm-hostname"); + Mockito.when(vm.getVncPassword()).thenReturn("vnc-password"); + Mockito.when(vm.getType()).thenReturn(VirtualMachine.Type.User); + Mockito.when(host.getHypervisorType()).thenReturn(Hypervisor.HypervisorType.KVM); + Mockito.when(vmInstanceDetailsDao.listDetailsKeyPairs(Mockito.anyLong(), Mockito.anyList())).thenReturn(Map.of()); + Mockito.when(managementServer.getVncPort(vm)).thenReturn(hostPortInfo); + Mockito.doReturn(new Ternary<>(hostAddress, null, null)) + .when(consoleAccessManager).parseHostInfo(Mockito.anyString()); + Mockito.doReturn(false).when(consoleAccessManager).isKvmMultipleConsoleViewersEnabled(); + + ConsoleConnectionDetails result = consoleAccessManager.getConsoleConnectionDetails(vm, host); + + Assert.assertNotNull(result); + Assert.assertFalse(result.isSessionRequiresNewViewer()); + } + + @Test + public void getConsoleConnectionDetailsDoesNotRequireNewViewerForKVMSystemVm() { + VirtualMachine vm = Mockito.mock(VirtualMachine.class); + HostVO host = Mockito.mock(HostVO.class); + String hostAddress = "192.168.1.100"; + int port = 5900; + Pair hostPortInfo = new Pair<>(hostAddress, port); + + Mockito.when(vm.getUuid()).thenReturn("vm-uuid"); + Mockito.when(vm.getHostName()).thenReturn("vm-hostname"); + Mockito.when(vm.getVncPassword()).thenReturn("vnc-password"); + Mockito.when(vm.getType()).thenReturn(VirtualMachine.Type.DomainRouter); + Mockito.when(host.getHypervisorType()).thenReturn(Hypervisor.HypervisorType.KVM); + Mockito.when(vmInstanceDetailsDao.listDetailsKeyPairs(Mockito.anyLong(), Mockito.anyList())).thenReturn(Map.of()); + Mockito.when(managementServer.getVncPort(vm)).thenReturn(hostPortInfo); + Mockito.doReturn(new Ternary<>(hostAddress, null, null)) + .when(consoleAccessManager).parseHostInfo(Mockito.anyString()); + ConsoleConnectionDetails result = consoleAccessManager.getConsoleConnectionDetails(vm, host); + + Assert.assertNotNull(result); + Assert.assertFalse(result.isSessionRequiresNewViewer()); + Mockito.verify(consoleAccessManager, Mockito.never()).isKvmMultipleConsoleViewersEnabled(); + } + @Test public void getConsoleConnectionDetailsReturnsDetailsWithRDPForHyperV() { VirtualMachine vm = Mockito.mock(VirtualMachine.class);