From mboxrd@z Thu Jan 1 00:00:00 1970 Return-Path: Received: from gate001.proxmox.com (gate001.proxmox.com [IPv6:2a0f:8001:1:32::40]) by lore.proxmox.com (Postfix) with ESMTPS id B914A1FF0C1 for ; Wed, 26 Aug 2026 10:17:58 +0200 (CEST) Received: from gate001.proxmox.com (localhost.localdomain [127.0.0.1]) by gate001.proxmox.com (Proxmox) with ESMTP id 7E4F521351; Wed, 26 Aug 2026 10:17:58 +0200 (CEST) ARC-Seal: i=1; a=rsa-sha256; s=arcselector10001; d=microsoft.com; cv=none; b=ZWn7ynijdpMEW23/8h2mDADV3u/7M8x553o33B9X3Hf78cB0fkVRLidzQoH+HFBpjVoBOYTlnPrAYuYONpW6IhUoydcDyEa8TJi7DghSDvUGUX8mD4rbjtx4D0k0x+grMremc/jM7Fqdq+KQb194ncLgeahlsKtTk0mYQF7EeoMQHTfHvuAPQL6W0r3Zjb34TKYw90knaye3/CQ4YwDDS56lIIhd708zacHv51jHFb2sDJpxAMC1UmsWkpDv/4mW5sXyn059y2k1vUY8mG0U4M2QHF9RgqY4E2v5oJoB5mD8Vf6b4x0LMJ/UDWzVjXDFrf12+x/o9BIfIq032OWq2w== ARC-Message-Signature: i=1; a=rsa-sha256; c=relaxed/relaxed; d=microsoft.com; s=arcselector10001; h=From:Date:Subject:Message-ID:Content-Type:MIME-Version:X-MS-Exchange-AntiSpam-MessageData-ChunkCount:X-MS-Exchange-AntiSpam-MessageData-0:X-MS-Exchange-AntiSpam-MessageData-1; bh=pga7Byghco+jTnGolE/x9FYJrv6t7Qp3GpqlyP4rRsA=; b=fui/NS0F4mcn4oUP0BTMwb8oVN9HwDZq2+1h8njzLSU4hBnIBGR/c+yW8bg2QhTK/LglK5TFJuj75/PWuAzgaoSh1fq5eN16mE81LF7rRdiGasnqX3n8cGKAnz+1Rb1EMJD+maTjbburO41EE/VjmTFGeT7D83oGhIajBiTGM3urqPOx1Nh4JKPseIwvIOqaxzwrDS4G10gfTrW714Vzg7bE9FdjkakmG8egG2ZuEa3aNpI17UDQgqMpdbbtU1MDM4WhCvbZVfP2qg7CIikVUZx8o4MlmZ/Noj2FP20kJVKv/2riXawwHkSPhL3lAO2vZ4JkREAMaVH9Uc96OTj7RQ== ARC-Authentication-Results: i=1; mx.microsoft.com 1; spf=pass smtp.mailfrom=groupe-cyllene.com; dmarc=pass action=none header.from=groupe-cyllene.com; dkim=pass header.d=groupe-cyllene.com; arc=none DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=groupecyllene.onmicrosoft.com; s=selector2-groupecyllene-onmicrosoft-com; h=From:Date:Subject:Message-ID:Content-Type:MIME-Version:X-MS-Exchange-SenderADCheck; bh=pga7Byghco+jTnGolE/x9FYJrv6t7Qp3GpqlyP4rRsA=; b=NvzrBw8+1ON392Bn6ZvqdctVurll5fYVuEo9qBAwGQ6tRBOkeJutScvRj2rklgbhrWNdFRsMxIeWS0agpIHKnlBsJBZdwuyhNNGWHeR5sikIINiPea7gy/IK1xx1JslNBnmoFOi4wIJk26LKVnCgpImGtDkXAt3Jm0LJQJ0Jbkg= Authentication-Results: dkim=none (message not signed) header.d=none;dmarc=none action=none header.from=groupe-cyllene.com; From: Alexandre Derumier To: pve-devel@lists.proxmox.com Subject: [RFC v2 pve-manager 06/13] ui: add kyber console Date: Wed, 26 Aug 2026 09:43:38 +0200 Message-ID: <20260826074347.1256659-7-alexandre.derumier@groupe-cyllene.com> X-Mailer: git-send-email 2.55.0 In-Reply-To: <20260826074347.1256659-1-alexandre.derumier@groupe-cyllene.com> References: <20260826074347.1256659-1-alexandre.derumier@groupe-cyllene.com> Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit X-ClientProxiedBy: PA7P264CA0229.FRAP264.PROD.OUTLOOK.COM (2603:10a6:102:372::8) To PR1P264MB3696.FRAP264.PROD.OUTLOOK.COM (2603:10a6:102:143::11) MIME-Version: 1.0 X-MS-PublicTrafficType: Email X-MS-TrafficTypeDiagnostic: PR1P264MB3696:EE_|PR0P264MB2257:EE_ X-MS-Office365-Filtering-Correlation-Id: 8171c6ab-e61b-44f3-09dc-08df0345d4f2 X-MS-Exchange-SenderADCheck: 1 X-MS-Exchange-AntiSpam-Relay: 0 X-Microsoft-Antispam: BCL:0;ARA:13230040|376014|23010399003|52116014|1800799024|366016|38350700014|6133799003|3023799007|56012099006|10067099003|5023799004|22082099003|18002099003; X-Microsoft-Antispam-Message-Info: MZy5OfKo9XXbn83fsZZuQRCoz6mFDqjaDutCpO3yXvkELCx0o5skBUi2vsv9Nuk6rBM/XUqOOcLY47fH1m92H2S8V1/uSpLisv/cL54vDZ2fk5SgQqQHqxClIQTrPW6UpA6istVSluV/ZsSOkQDTD6WRypZQr0vWz/G/zr+meEqwHDo0oKkskV4x1+Ag4WwFxLyMkjiiyc22bP2U7BLcVlWm+d+k6Vx3DNJ2XmxsIqXfi9W51AC8xWDtWDDxjhOVj2/hROXSaQZc80qwuRFJzaJ5w+4GSQYFxghloZrJPD9Q/hPLjujg8ILzEdioS2iV7av/1teZftOUYEO1hfEth+u2OuNJvd+L7kHm0mXLFVGcSdGG84AOYfL8bq+DsA5iy+yrYScPjNh2oaHdInzqameAdhYxe3/dZYz0QsGWGJHf4/+loPJM4CinItFbgCR1c9fZX8EK/ENUYxw2Chx10eNBUNxIgDsWGS8LejNEz5n8FrcyoaIaGIsq5QtslCRAOflhJlZ36OP+rQOMEjsu31BK1r4UafV8iziGTlvKBqgzmawx8SgupKiyhIAWkBNbzZHgpeWQd9pIp2vWF+5ophPxOBL9wTxTqdf3cpKtYl5RAxtear3dy+U6e5wN0fwcOhlHCet27CNjbdLuO9dsmyj/oydfhugbvBreWy9gSnbwyu3UiVSjRZ8u796CI+nx86GtleymH5I7dM0tYeJ2ExtYWsLYu4PE6eCy1wm6TzI= X-Forefront-Antispam-Report: CIP:255.255.255.255;CTRY:;LANG:en;SCL:1;SRV:;IPV:NLI;SFV:NSPM;H:PR1P264MB3696.FRAP264.PROD.OUTLOOK.COM;PTR:;CAT:NONE;SFS:(13230040)(376014)(23010399003)(52116014)(1800799024)(366016)(38350700014)(6133799003)(3023799007)(56012099006)(10067099003)(5023799004)(22082099003)(18002099003);DIR:OUT;SFP:1102; X-MS-Exchange-AntiSpam-MessageData-ChunkCount: 1 X-MS-Exchange-AntiSpam-MessageData-0: =?utf-8?B?SjduNmVUbjVxTzRqSFlmVmo4NDNubE4zV25GQ3ZndlRWK1B0dVdYRzVWL21s?= =?utf-8?B?bDZ4T2NSRGh5SFBhdUtJSXdWc2lLTkVxZWRzNWRIbGl4V0xlY1VvbDczaDkr?= =?utf-8?B?cTJibC83RHV5dVFNZCtTUWNMajl1cVNSRWYrUStXcVpkbHdFMzlzMmY3MDZq?= =?utf-8?B?WXE1N0RqMnk2TmliZEtwN3BueU9sajE0Ri9Uc0M0eUdUMFpzYytiekR3MjRG?= =?utf-8?B?L3BJZTB3YUlNU3BuRkxUL00vVXhZb042NmQ2UjR6Ym84bmZ3U3l6YVpCZkxk?= =?utf-8?B?Rm1FNnJVS0xRYmVaNmZpQy9XRGpaK0RxMWNMbkdRdk13UHB1YkZtRHhEZnJj?= =?utf-8?B?UkZqVXIwUG5vNEFKVEpYT05kVnhPMGxLUGZta0pVbTAxMVJZWWpvN1laSWRa?= =?utf-8?B?elI2Y0k4bGhrTEtLWHlWenUwY25xOW5ielNKOHVhZUVYV29FTzh2TjVSMEVC?= =?utf-8?B?OGRnYncyYXhmNGhXaUlpWDRaZzJOMCthSjdkcHZTVmNHSXYraDVqMlRibk5l?= =?utf-8?B?eGQ4QmNOb1E4RDhDd014ZFNZVzhKTEthTDhsS3ZQNTczcUtsek1yZzhnSHZm?= =?utf-8?B?a0RadDZVSFRVaDdiOG1STGJ1MFpVRmIwL3MxM2F6MDFpSlJpYjBGKzNqcHBr?= =?utf-8?B?eGpVUnhndG1Fb01FL3BMOG1kajU2U3hPTGEyU1FxekpqeUx3ZVFUaVB3Q2RL?= =?utf-8?B?NnZCRGF0dWNxcnN5WGNEa1Vpc1JZMUJ1UEd5UzA5bmhocE5WK2NlOEZGaXNZ?= =?utf-8?B?blZnT2RUbGQ2R0hDSkNEUjc3aWRITGN6SW1XVGQxZCsveUhWaDZvVW5US2Fz?= =?utf-8?B?SjlkbVlOM3BINE1zek9abHRCTUU4RGhWZElYS2h3MzZkL2phSVQrREZDazFh?= =?utf-8?B?TVVFejVpTytINEJlSEQzS2RqWXphekkydWNrUGo0RFhCbFhZMEZxekdNME1E?= =?utf-8?B?czF2MVpTMjkrSzA5cVVkMnpJbFRrMlhWRi95MTZIMXNYUXpPRldBM1FUaWVw?= =?utf-8?B?VzhrRmVVLzcrckxlajNzSGhycEZ0ZCtqZDdkeTYvdENrSUtkdE9TaDltSE52?= =?utf-8?B?S2JSNUVLZmgxTDEzdDl1cmM4Y0QxOElUVVk0VnE4MmtWbkplWWRLaWw4SUZo?= =?utf-8?B?Q1FWOXRxeUIxK2hIYTVEV0U3R0d2b090TUprK3NvOTl2RWpOTm93dkFOQUEy?= =?utf-8?B?bnN0NlFubXJkNzFyOHVKTXViNjU4K1B3MUFkQWhmVGhxMnVxaHhNZTA1ZGVw?= =?utf-8?B?MXZveGhCa2MvV295OEQ5LzlRZUszeVpJeEU3MThzNE1idmpZbForY1lPRmZs?= =?utf-8?B?YUhraWRDVFk1TE5DWEJLWmx1UmRhV2Nld0p3WXF3bFV5MVNmbHVlZnpNMy9a?= =?utf-8?B?M0plUDVOZmVVWUFjUExUbSswcG44TG9CRWMwUnFFa3E4YzV1OGI2aUxyZ2RC?= =?utf-8?B?TGplTkJVNWZxUVVJc3lqQ3VpeThwdTgzVTJaUXNUS2h5Qnd1N2ZSbExSNzRX?= =?utf-8?B?R1AwMEVEeFQvM2RQQ2QzWVZsQmpPUXlEY3NmTldvRGUvM1cvZnRIelprcmh5?= =?utf-8?B?VW41Z0hpL21YQU9wV1d1NXp2Z3pVd1A3aXVjdVRzVGIyVWJQek8xSEVQd1RP?= =?utf-8?B?cytERGd6NEMvYzdicVc1L1JtOGo1aHo5WFZhaWhvZEtUbHFUaTJKRFZwQmRS?= =?utf-8?B?L3dJZFdJRFZpZi9INmsvNm11ZlJyNTNNaXBTUkdveVd5ZFVNYzFLRmRRcVI4?= =?utf-8?B?SXZBZUEvMTE4MGxNTmNqUnE1MnFrT0hQZ3YwTmxzR0tabCt3OGRuU0MwVzVr?= =?utf-8?B?eUM1SXVzSzNKY3g2M0RqZmE1ZDBsblRDTkh2QVlDUTBILzE0OTAweCtyU2lX?= =?utf-8?B?RmU3dEpGVTlwVVRWYXY1MnZXdy9mVXZrbHlDVUh5cDNjWC9DMVFlTEZXZitp?= =?utf-8?B?Y0gvMDNTQWhVN3FFTFM3VzArR3FRMG9pRWtNU253cTczUnZneE5xUmF2cEts?= =?utf-8?B?OHRXVk5LZEg1Zit2ZXJINktrU2NBSk41STN5QmpMeXNFNVJSK2twUisxS0lQ?= =?utf-8?B?TURhWHQ5Yk9CR0g3bWlaM2tRR1NhN29IaFRTdjNtQmY2ZVFKMnRXb00yLzdK?= =?utf-8?B?cFIyWFJrd29VWldMV3VsZVFOVzR3Q1JtS1dDTWl2TmE2RnBXQUhRNUVrcmVS?= =?utf-8?B?ZFRUa3ZYUXJ1UjY5TGJNTlBxcUthRVBqRmRibTdzSGNPcXN2SkYrKyt6YUZE?= =?utf-8?B?NnlLNStabkRGQnhpWW94Mzd2NlMxRjFkMVd2b3lCTUNOYTRZcTFtNmIwUFVL?= =?utf-8?B?TUlKUVIybWF4aHNHSzdhK2FFcE5RTEpOVnZMMWppM09UMzdSME1UNWNnZmF4?= =?utf-8?Q?Lv+JNHeeSStsF/4P9a9To5zI8++KDh/S2zF8M?= X-OriginatorOrg: groupe-cyllene.com X-MS-Exchange-CrossTenant-Network-Message-Id: 8171c6ab-e61b-44f3-09dc-08df0345d4f2 X-MS-Exchange-CrossTenant-AuthSource: PR1P264MB3696.FRAP264.PROD.OUTLOOK.COM X-MS-Exchange-CrossTenant-AuthAs: Internal X-MS-Exchange-CrossTenant-OriginalArrivalTime: 26 Aug 2026 07:44:17.2012 (UTC) X-MS-Exchange-CrossTenant-FromEntityHeader: Hosted X-MS-Exchange-CrossTenant-Id: ee11ccf7-112c-4284-848b-f229745e715b X-MS-Exchange-CrossTenant-MailboxType: HOSTED X-MS-Exchange-CrossTenant-UserPrincipalName: sFBPCK9SAFarFvwXmApR2UChi7BSDYxNCzvl3lFCP77rWg10YT4ZTa1eTnNTGNDHeC3A4jkzxmVfB/l5hwoQd+WCZCVBrjNRtbfneVrAeHA44u6XUQ4lqBcCj68P6BST X-MS-Exchange-Transport-CrossTenantHeadersStamped: PR0P264MB2257 X-SPAM-LEVEL: Spam detection results: 0 AWL 0.230 Adjusted score from AWL reputation of From: address CONTENT_AFTER_HTML 0.001 More content after HTML close tag + other spam signs DKIM_SIGNED 0.1 Message has a DKIM or DK signature, not necessarily valid DKIM_VALID -0.1 Message has at least one valid DKIM or DK signature DMARC_PASS -0.1 DMARC pass policy RCVD_IN_DNSWL_NONE -0.0001 Sender listed at https://www.dnswl.org/, no trust RCVD_IN_MSPIKE_H2 0.001 Average reputation (+2) SPF_HELO_PASS -0.001 SPF: HELO matches SPF record SPF_PASS -0.001 SPF: sender matches SPF record Message-ID-Hash: CAIJ656OPM66IUPTDIGYTWYEBGF72SWF X-Message-ID-Hash: CAIJ656OPM66IUPTDIGYTWYEBGF72SWF X-MailFrom: Alexandre.DERUMIER@groupe-cyllene.com X-Mailman-Rule-Misses: dmarc-mitigation; no-senders; approved; loop; banned-address; emergency; member-moderation; nonmember-moderation; administrivia; implicit-dest; max-recipients; max-size; news-moderation; no-subject; digests; suspicious-header X-Mailman-Version: 3.3.10 Precedence: list List-Id: Proxmox VE development discussion List-Help: List-Owner: List-Post: List-Subscribe: List-Unsubscribe: Signed-off-by: Alexandre Derumier --- Makefile | 2 +- PVE/HTTPServer.pm | 4 +- PVE/Service/pveproxy.pm | 53 +++- kyber-web/Makefile | 22 ++ kyber-web/index.html.tpl | 438 +++++++++++++++++++++++++++ www/manager6/Utils.js | 30 +- www/manager6/button/ConsoleButton.js | 22 ++ www/manager6/qemu/AudioEdit.js | 3 + www/manager6/qemu/Config.js | 9 +- www/manager6/qemu/DisplayEdit.js | 4 +- 10 files changed, 581 insertions(+), 6 deletions(-) create mode 100644 kyber-web/Makefile create mode 100644 kyber-web/index.html.tpl diff --git a/Makefile b/Makefile index 6c8b82e..b7dfc7f 100644 --- a/Makefile +++ b/Makefile @@ -10,7 +10,7 @@ DSC=$(PACKAGE)_$(DEB_VERSION).dsc DEB=$(PACKAGE)_$(DEB_VERSION)_all.deb DESTDIR= -SUBDIRS = aplinfo PVE bin www services configs network-hooks test templates +SUBDIRS = aplinfo PVE bin www services configs kyber-web network-hooks test templates all: $(SUBDIRS) set -e && for i in $(SUBDIRS); do $(MAKE) -C $$i; done diff --git a/PVE/HTTPServer.pm b/PVE/HTTPServer.pm index 34403e8..c3fef48 100755 --- a/PVE/HTTPServer.pm +++ b/PVE/HTTPServer.pm @@ -120,11 +120,13 @@ sub auth_handler { $isUpload = 1; } + my $is_kyber_console = $rel_uri =~ m|^/nodes/[^/]+/qemu/\d+/kyber/|; + # Skip CSRF check for file upload (difficult to pass CSRF header with native html forms). # Also skip the check with API tokens, as one of the design goals of API tokens was to # provide stateless API access without requiring round-trips to get such CSRF tokens. # CSRF-prevention also does not make much sense outside of the browser context. - if ($method ne 'GET' && !($api_token || $isUpload)) { + if ($method ne 'GET' && !($api_token || $isUpload || $is_kyber_console)) { my $euid = $>; PVE::AccessControl::verify_csrf_prevention_token($username, $token) if $euid != 0; } diff --git a/PVE/Service/pveproxy.pm b/PVE/Service/pveproxy.pm index dfdd014..bcc1353 100755 --- a/PVE/Service/pveproxy.pm +++ b/PVE/Service/pveproxy.pm @@ -20,6 +20,10 @@ use PVE::Cluster; use PVE::Daemon; use PVE::DataCenterConfig; use PVE::HTTPServer; +use HTTP::Status qw(HTTP_BAD_REQUEST); +use PVE::Exception; +use PVE::INotify; +use PVE::RPCEnvironment; use PVE::SafeSyslog; use PVE::pvecfg; use PVE::Tools; @@ -53,6 +57,7 @@ my $basedirs = { fontlogos => '/usr/share/fonts-font-logos', i18n => '/usr/share/pve-i18n', manager => '/usr/share/pve-manager', + kyber => '/usr/share/pve-kyber-web', novnc => '/usr/share/novnc-pve', yew_mobile => '/usr/share/pve-yew-mobile-gui', i18n_yew => '/usr/share/pve-yew-mobile-i18n', @@ -60,6 +65,41 @@ my $basedirs = { xtermjs => '/usr/share/pve-xtermjs', }; +my $kyber_console_prefix = qr!^/api2/json/nodes/([^/]+)/qemu/(\d+)/kyber(/.*)$!; +my $kyber_proxy_socket = '/run/pvekyberproxy.sock'; + +my sub check_console_access { + my ($auth, $console_type, $node, $vmid) = @_; + + PVE::Exception::raise( + "the $console_type console for VM $vmid must be opened on node '$node'\n", + code => HTTP_BAD_REQUEST, + ) if $node ne PVE::INotify::nodename(); + + my $rpcenv = PVE::RPCEnvironment::get(); + $rpcenv->check($auth->{userid}, "/vms/$vmid", ['VM.Console']); + + return; +} + +sub console_proxy { + my ($server, $reqstate, $auth, $method, $path) = @_; + + if (my ($node, $vmid, $rest) = $path =~ $kyber_console_prefix) { + check_console_access($auth, 'Kyber', $node, $vmid); + + my $target = $path; + + if (my $query = $reqstate->{request}->url->query()) { + $target .= "?$query"; + } + + return { socket => $kyber_proxy_socket, path => $target, tls => 0 }; + } + + return undef; +} + sub init { my ($self) = @_; @@ -77,6 +117,7 @@ sub init { my $dirs = {}; + add_dirs($dirs, '/kyber/' => "$basedirs->{kyber}/"); add_dirs($dirs, '/novnc/' => "$basedirs->{novnc}/"); add_dirs($dirs, '/pve-docs/' => "$basedirs->{docs}/"); add_dirs($dirs, '/pve-docs/api-viewer/extjs/' => "$basedirs->{extjs}/"); @@ -134,6 +175,7 @@ sub init { }, }, dirs => $dirs, + local_http_proxy_handler => \&console_proxy, }; if (defined($proxyconf->{DHPARAMS})) { @@ -247,6 +289,7 @@ sub get_index { (is_phone($r->header('User-Agent')) && (!defined($args->{mobile}) || $args->{mobile})) || $args->{mobile}; + my $kyber = defined($args->{console}) && $args->{kyber}; my $novnc = defined($args->{console}) && $args->{novnc}; my $xtermjs = defined($args->{console}) && $args->{xtermjs}; @@ -291,7 +334,9 @@ sub get_index { # by default, load the normal index my $dir = $basedirs->{manager}; - if ($novnc) { + if ($kyber) { + $dir = $basedirs->{kyber}; + } elsif ($novnc) { $dir = $basedirs->{novnc}; } elsif ($xtermjs) { $dir = $basedirs->{xtermjs}; @@ -305,6 +350,12 @@ sub get_index { $template->process("$dir/index.html.tpl", $vars, \$page) || die $template->error(), "\n"; my $headers = HTTP::Headers->new(Content_Type => "text/html; charset=utf-8"); + + if ($kyber) { + $headers->header('Cross-Origin-Opener-Policy' => 'same-origin'); + $headers->header('Cross-Origin-Embedder-Policy' => 'require-corp'); + } + my $resp = HTTP::Response->new(200, "OK", $headers, $page); return $resp; diff --git a/kyber-web/Makefile b/kyber-web/Makefile new file mode 100644 index 0000000..28b07a0 --- /dev/null +++ b/kyber-web/Makefile @@ -0,0 +1,22 @@ +include ../defines.mk + +KYBERDIR = $(DESTDIR)/usr/share/pve-kyber-web + +all: + +.PHONY: install +install: index.html.tpl + install -d $(KYBERDIR) + install -m 0644 index.html.tpl $(KYBERDIR)/index.html.tpl +# The client itself - kyclient_wasm.js, kyclient_wasm_bg.wasm and the audio +# worklet beside them - is built from kyber-web and shipped by its own package +# rather than vendored here. It is AGPL-3.0-or-later, same as pve-manager, but +# it is a separate upstream with its own release cadence and a wasm toolchain +# that has no business in this build. +# +# The page drives that SDK directly and needs nothing else from it: no bundle, +# no stylesheets, and none of the demo client's UI. + +.PHONY: clean distclean +distclean: clean +clean: diff --git a/kyber-web/index.html.tpl b/kyber-web/index.html.tpl new file mode 100644 index 0000000..31f5e76 --- /dev/null +++ b/kyber-web/index.html.tpl @@ -0,0 +1,438 @@ + + + + + Kyber console + + + + + +
+
+
+ + + +
+ + + + + +
+
+
+
+
Connecting…
+ + + + diff --git a/www/manager6/Utils.js b/www/manager6/Utils.js index c86a00c..44ffdfe 100644 --- a/www/manager6/Utils.js +++ b/www/manager6/Utils.js @@ -656,6 +656,7 @@ Ext.define('PVE.Utils', { serial3: gettext('Serial terminal') + ' 3', virtio: 'VirtIO-GPU', 'virtio-gl': 'VirGL GPU', + kyber: 'Kyber', none: Proxmox.Utils.noneText, }, @@ -1452,6 +1453,8 @@ Ext.define('PVE.Utils', { PVE.Utils.openVNCViewer(consoleType, vmid, nodename, vmname, cmd); } else if (viewer === 'xtermjs') { Proxmox.Utils.openXtermJsViewer(consoleType, vmid, nodename, vmname, cmd); + } else if (viewer === 'kyber') { + PVE.Utils.openKyberViewer(consoleType, vmid, nodename, vmname); } else if (viewer === 'vv') { let url = '/nodes/' + nodename + '/spiceshell'; let params = { @@ -1475,16 +1478,22 @@ Ext.define('PVE.Utils', { }, defaultViewer: function (consoles, type) { - var allowSpice, allowXtermjs; + var allowSpice, allowXtermjs, allowKyber; if (consoles === true) { allowSpice = true; allowXtermjs = true; + allowKyber = true; } else if (typeof consoles === 'object') { allowSpice = consoles.spice; allowXtermjs = !!consoles.xtermjs; + allowKyber = !!consoles.kyber; } let dv = PVE.UIOptions.options.console || (type === 'kvm' ? 'vv' : 'xtermjs'); + // A Kyber display serves no VNC, so nothing else can show it. + if (allowKyber) { + return 'kyber'; + } if (dv === 'vv' && !allowSpice) { dv = allowXtermjs ? 'xtermjs' : 'html5'; } else if (dv === 'xtermjs' && !allowXtermjs) { @@ -1515,6 +1524,24 @@ Ext.define('PVE.Utils', { } }, + // The Kyber console streams the display over its own controller rather + // than through noVNC, so it gets a window of its own. Everything it + // needs is fetched by that page from kyberproxy; nothing is passed in + // the URL, so a copied link grants nothing on its own. + openKyberViewer: function (vmtype, vmid, nodename, vmname) { + let url = Ext.Object.toQueryString({ + console: vmtype, + kyber: 1, + vmid: vmid, + vmname: vmname, + node: nodename, + }); + let nw = window.open('?' + url, '_blank', 'innerWidth=1280,innerheight=800'); + if (nw) { + nw.focus(); + } + }, + openSpiceViewer: function (url, params) { var downloadWithName = function (uri, name) { var link = Ext.DomHelper.append(document.body, { @@ -1590,6 +1617,7 @@ Ext.define('PVE.Utils', { let consoles = { spice: !!conf.spice, xtermjs: !!conf.serial, + kyber: !!conf.kyber, }; PVE.Utils.openDefaultConsoleWindow(consoles, 'kvm', vmid, nodename, vmname); }, diff --git a/www/manager6/button/ConsoleButton.js b/www/manager6/button/ConsoleButton.js index d64f280..63377e7 100644 --- a/www/manager6/button/ConsoleButton.js +++ b/www/manager6/button/ConsoleButton.js @@ -12,6 +12,8 @@ Ext.define('PVE.button.ConsoleButton', { enableSpice: true, enableXtermjs: true, + // Off unless a VM says otherwise, so other guests show it greyed out. + enableKyber: false, nodename: undefined, @@ -33,6 +35,13 @@ Ext.define('PVE.button.ConsoleButton', { me.down('#xtermjs').setDisabled(!enable); }, + setEnableKyber: function (enable) { + var me = this; + + me.enableKyber = enable; + me.down('#kybermenu').setDisabled(!enable); + }, + handler: function () { // main, general, handler let me = this; @@ -40,6 +49,7 @@ Ext.define('PVE.button.ConsoleButton', { { spice: me.enableSpice, xtermjs: me.enableXtermjs, + kyber: me.enableKyber, }, me.consoleType, me.vmid, @@ -84,6 +94,18 @@ Ext.define('PVE.button.ConsoleButton', { view.openConsole(button.type); }, }, + { + xtype: 'menuitem', + itemId: 'kybermenu', + text: 'Kyber', + type: 'kyber', + iconCls: 'fa fa-fw fa-desktop', + disabled: true, + handler: function (button) { + let view = this.up('button'); + view.openConsole(button.type); + }, + }, { text: 'xterm.js', itemId: 'xtermjs', diff --git a/www/manager6/qemu/AudioEdit.js b/www/manager6/qemu/AudioEdit.js index ba588ed..49f03e1 100644 --- a/www/manager6/qemu/AudioEdit.js +++ b/www/manager6/qemu/AudioEdit.js @@ -35,6 +35,9 @@ Ext.define('PVE.qemu.AudioInputPanel', { fieldLabel: gettext('Backend Driver'), comboItems: [ ['spice', 'SPICE'], + // Puts the guest's audio on the same D-Bus as its display, + // which is where the Kyber and RDP consoles read it. + ['dbus', gettext('D-Bus (Kyber/RDP console)')], ['none', `${Proxmox.Utils.NoneText} (${gettext('Dummy Device')})`], ], }, diff --git a/www/manager6/qemu/Config.js b/www/manager6/qemu/Config.js index 842d35d..bcf54d1 100644 --- a/www/manager6/qemu/Config.js +++ b/www/manager6/qemu/Config.js @@ -232,9 +232,10 @@ Ext.define('PVE.qemu.Config', { disabled: !caps.vms['VM.Console'], hidden: template, consoleType: 'kvm', - // disable spice/xterm for default action until status api call succeeded + // disable spice/xterm/kyber for default action until status api call succeeded enableSpice: false, enableXtermjs: false, + enableKyber: false, consoleName: vm.name, nodename: nodename, vmid: vmid, @@ -458,6 +459,7 @@ Ext.define('PVE.qemu.Config', { var qmpstatus; var spice = false; var xtermjs = false; + var kyber = false; var lock; var rec; @@ -475,6 +477,10 @@ Ext.define('PVE.qemu.Config', { spice = !!s.data.get('spice'); xtermjs = !!s.data.get('serial'); + // Reported by the status API only for a display set to + // 'kyber', which is also the only case with a controller + // behind it. + kyber = !!s.data.get('kyber'); } rec = s.data.get('tags'); @@ -496,6 +502,7 @@ Ext.define('PVE.qemu.Config', { consoleBtn.setEnableSpice(spice); consoleBtn.setEnableXtermJS(xtermjs); + consoleBtn.setEnableKyber(kyber); statusTxt.update({ lock: lock }); diff --git a/www/manager6/qemu/DisplayEdit.js b/www/manager6/qemu/DisplayEdit.js index 3f583ad..79e1ea2 100644 --- a/www/manager6/qemu/DisplayEdit.js +++ b/www/manager6/qemu/DisplayEdit.js @@ -26,7 +26,9 @@ Ext.define('PVE.qemu.DisplayInputPanel', { return '4'; } else if (val === 'std' || val.match(/^qxl\d?$/) || val === 'vmware') { return '16'; - } else if (val.match(/^virtio/)) { + } else if (val.match(/^virtio/) || val === 'kyber') { + // kyber is a virtio-vga underneath, so it takes the same + // memory as one. return '256'; } else if (get('matchNonGUIOption')) { return 'N/A'; -- 2.55.0