From b6775d68a98cc2c6ec3f705810050cabd9889d40 Mon Sep 17 00:00:00 2001 From: Wei Zhou Date: Fri, 2 Oct 2026 09:36:25 +0200 Subject: [PATCH] ca: reload VNC TLS certificate on VMs when provisioning certificate via SSH When provisioning a certificate on a KVM host via SSH (forced mode, when agent is disconnected), live-reload the VNC TLS certificate on all running VMs before restarting libvirtd and cloudstack-agent. This mirrors the behavior of the agent-connected path (PostCertificateRenewalCommand) which uses QMP display-reload to reload the certificate on running VMs instead of relying on libvirtd restart alone. Without this, running VMs on the host would keep using the old certificate until they are stopped/started or migrated. Adds: - reloadVncTlsCertificateOnRunningVmsViaSsh: Uses virsh to list running domains and issues the QMP display-reload command with tls-certs=true for each VM. Per-VM failures are tolerated and logged, not thrown. --- .../org/apache/cloudstack/ca/CAManagerImpl.java | 17 +++++++++++++++++ .../apache/cloudstack/ca/CAManagerImplTest.java | 9 +++++++-- 2 files changed, 24 insertions(+), 2 deletions(-) diff --git a/server/src/main/java/org/apache/cloudstack/ca/CAManagerImpl.java b/server/src/main/java/org/apache/cloudstack/ca/CAManagerImpl.java index 73ff79301fb7..630a43cbc6a8 100644 --- a/server/src/main/java/org/apache/cloudstack/ca/CAManagerImpl.java +++ b/server/src/main/java/org/apache/cloudstack/ca/CAManagerImpl.java @@ -325,6 +325,7 @@ private boolean provisionKvmHostViaSsh(Host host, String caProvider) { provisionCertificateViaSsh(sshConnection, hostIp, host.getName(), caProvider); String sudoPrefix = "root".equals(username) ? "" : "sudo "; + reloadVncTlsCertificateOnRunningVmsViaSsh(sshConnection, sudoPrefix, hostIp); SSHCmdHelper.sshExecuteCmd(sshConnection, sudoPrefix + "systemctl restart libvirtd"); SSHCmdHelper.sshExecuteCmd(sshConnection, sudoPrefix + "systemctl restart cloudstack-agent"); @@ -339,6 +340,22 @@ private boolean provisionKvmHostViaSsh(Host host, String caProvider) { } } + /** + * Live-reloads the VNC TLS certificate on every running VM via SSH, since a libvirtd/cloudstack-agent restart + * alone does not affect VMs already running. Per-VM failures are tolerated and logged, not thrown. + */ + private void reloadVncTlsCertificateOnRunningVmsViaSsh(final Connection sshConnection, final String sudoPrefix, final String hostIp) { + final String cmd = sudoPrefix + "virsh -c qemu:///system list --name --state-running | while read -r vm; do " + + "[ -z \"$vm\" ] && continue; " + + sudoPrefix + "virsh -c qemu:///system qemu-monitor-command \"$vm\" " + + "'{\"execute\":\"display-reload\",\"arguments\":{\"type\":\"vnc\",\"tls-certs\":true}}' >/dev/null 2>&1 " + + "|| echo \"failed to reload VNC TLS certificate for VM $vm\" >&2; done"; + final SSHCmdHelper.SSHCmdResult result = SSHCmdHelper.sshExecuteCmdWithResult(sshConnection, cmd); + if (!result.isSuccess()) { + logger.warn("Failed to reload VNC TLS certificate on running VMs via SSH on host: {}, error: {}", hostIp, result.getStdErr()); + } + } + private boolean provisionSystemVmViaSsh(Host host, Boolean reconnect, String caProvider) { VMInstanceVO vm = vmInstanceDao.findVMByInstanceName(host.getName()); if (vm == null) { diff --git a/server/src/test/java/org/apache/cloudstack/ca/CAManagerImplTest.java b/server/src/test/java/org/apache/cloudstack/ca/CAManagerImplTest.java index 2d60833d35c7..8dceba56c62e 100644 --- a/server/src/test/java/org/apache/cloudstack/ca/CAManagerImplTest.java +++ b/server/src/test/java/org/apache/cloudstack/ca/CAManagerImplTest.java @@ -281,6 +281,10 @@ public void testProvisionKvmHostViaSsh() throws Exception { MockedStatic sshCmdHelperMock = Mockito.mockStatic(SSHCmdHelper.class)) { sshCmdHelperMock.when(() -> SSHCmdHelper.acquireAuthorizedConnectionWithPublicKey(Mockito.any(Connection.class), Mockito.anyString(), Mockito.anyString())) .thenReturn(true); + sshCmdHelperMock.when(() -> SSHCmdHelper.sshExecuteCmdWithResult(Mockito.any(Connection.class), Mockito.contains("virsh"))) + .thenReturn(new SSHCmdHelper.SSHCmdResult(0, "", "")); + sshCmdHelperMock.when(() -> SSHCmdHelper.sshExecuteCmd(Mockito.any(Connection.class), Mockito.anyString())) + .thenReturn(true); Mockito.doNothing().when(caManager).provisionCertificateViaSsh(Mockito.any(Connection.class), Mockito.anyString(), Mockito.anyString(), Mockito.anyString()); @@ -290,8 +294,9 @@ public void testProvisionKvmHostViaSsh() throws Exception { Assert.assertTrue(result); Mockito.verify(caManager, Mockito.times(1)).provisionCertificateViaSsh(Mockito.any(Connection.class), Mockito.eq("192.168.1.1"), Mockito.eq("host1"), Mockito.eq("root")); - sshCmdHelperMock.verify(() -> SSHCmdHelper.sshExecuteCmd(Mockito.any(Connection.class), Mockito.eq("systemctl restart libvirtd")), Mockito.times(1)); - sshCmdHelperMock.verify(() -> SSHCmdHelper.sshExecuteCmd(Mockito.any(Connection.class), Mockito.eq("systemctl restart cloudstack-agent")), Mockito.times(1)); + sshCmdHelperMock.verify(() -> SSHCmdHelper.sshExecuteCmdWithResult(Mockito.any(Connection.class), Mockito.contains("virsh")), Mockito.times(1)); + sshCmdHelperMock.verify(() -> SSHCmdHelper.sshExecuteCmd(Mockito.any(Connection.class), Mockito.contains("systemctl restart libvirtd")), Mockito.times(1)); + sshCmdHelperMock.verify(() -> SSHCmdHelper.sshExecuteCmd(Mockito.any(Connection.class), Mockito.contains("systemctl restart cloudstack-agent")), Mockito.times(1)); } }