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);