This is an automated email from the ASF dual-hosted git repository.

RosiKyu pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/cloudstack.git


The following commit(s) were added to refs/heads/main by this push:
     new 2f34b0cd248 Allow multiple simultaneous console viewers for KVM user 
VMs (#13870)
2f34b0cd248 is described below

commit 2f34b0cd2483657041dd04a17ce86f116a4ecf93
Author: Eric Holm <[email protected]>
AuthorDate: Fri Nov 13 03:40:59 2026 -0800

    Allow multiple simultaneous console viewers for KVM user VMs (#13870)
    
    * Allow multiple console viewers for KVM user VMs
    
    * Clarify KVM system VM console viewer test
    
    * Fix release note formatting
---
 PendingReleaseNotes                                |  5 ++
 .../consoleproxy/ConsoleAccessManager.java         |  7 ++
 .../consoleproxy/ConsoleAccessManagerImpl.java     | 14 +++-
 .../consoleproxy/ConsoleAccessManagerImplTest.java | 74 ++++++++++++++++++++++
 4 files changed, 99 insertions(+), 1 deletion(-)

diff --git a/PendingReleaseNotes b/PendingReleaseNotes
index 823a13bd6ae..c6992f2cf7d 100644
--- a/PendingReleaseNotes
+++ b/PendingReleaseNotes
@@ -119,3 +119,8 @@ example.ver.1 > example.ver.2:
     utilisation comes from a moving average configured with 
host.load.sample.interval and
     host.load.half.life. Until a management server has collected samples, 
ranking falls back to
     allocation figures alone.
+
+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 655b8faf443..4bdd2cc5edd 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<Boolean> 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 237135b5176..237f83053ac 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 class ConsoleAccessManagerImpl extends ManagerBase 
implements ConsoleAcce
     public ConfigKey<?>[] getConfigKeys() {
         return new ConfigKey[] {
                 ConsoleAccessManager.ConsoleSessionCleanupInterval,
-                ConsoleAccessManager.ConsoleSessionCleanupRetentionHours
+                ConsoleAccessManager.ConsoleSessionCleanupRetentionHours,
+                ConsoleAccessManager.KvmMultipleConsoleViewersEnabled
         };
     }
 
@@ -474,6 +475,10 @@ public class ConsoleAccessManagerImpl extends ManagerBase 
implements ConsoleAcce
         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 @@ public class ConsoleAccessManagerImpl extends ManagerBase 
implements ConsoleAcce
                     .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 97e6295da1a..5d9f5bd8987 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 class ConsoleAccessManagerImplTest {
         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<String, Integer> 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<String, Integer> 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<String, Integer> 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);

Reply via email to