From 7abc435df37abf093eee4b96e8f0ef54650c2696 Mon Sep 17 00:00:00 2001 From: Ericreeses Date: Wed, 12 Aug 2026 19:54:46 +0000 Subject: [PATCH 1/2] Allow multiple console viewers for KVM user VMs --- PendingReleaseNotes | 5 ++ .../consoleproxy/ConsoleAccessManager.java | 7 ++ .../ConsoleAccessManagerImpl.java | 14 +++- .../ConsoleAccessManagerImplTest.java | 74 +++++++++++++++++++ 4 files changed, 99 insertions(+), 1 deletion(-) 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..db8be2136348 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 getConsoleConnectionDetailsDoesNotRequireNewViewerForKVMSystemVmWhenMultipleViewersEnabled() { + 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()); + } + @Test public void getConsoleConnectionDetailsReturnsDetailsWithRDPForHyperV() { VirtualMachine vm = Mockito.mock(VirtualMachine.class); From d9856208b852d77023cd780f9e7952aed95e4164 Mon Sep 17 00:00:00 2001 From: Ericreeses Date: Thu, 13 Aug 2026 12:51:09 +0000 Subject: [PATCH 2/2] Clarify KVM system VM console viewer test --- .../cloudstack/consoleproxy/ConsoleAccessManagerImplTest.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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 db8be2136348..5d9f5bd8987a 100644 --- a/server/src/test/java/org/apache/cloudstack/consoleproxy/ConsoleAccessManagerImplTest.java +++ b/server/src/test/java/org/apache/cloudstack/consoleproxy/ConsoleAccessManagerImplTest.java @@ -596,7 +596,7 @@ public void getConsoleConnectionDetailsDoesNotRequireNewViewerForKVMUserVmWhenMu } @Test - public void getConsoleConnectionDetailsDoesNotRequireNewViewerForKVMSystemVmWhenMultipleViewersEnabled() { + public void getConsoleConnectionDetailsDoesNotRequireNewViewerForKVMSystemVm() { VirtualMachine vm = Mockito.mock(VirtualMachine.class); HostVO host = Mockito.mock(HostVO.class); String hostAddress = "192.168.1.100"; @@ -612,11 +612,11 @@ public void getConsoleConnectionDetailsDoesNotRequireNewViewerForKVMSystemVmWhen 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