* [pve-devel] [PATCH pve-xtermjs 1/1] xtermjs: add support for remote node shells via PDM
2025-11-05 14:13 [pve-devel] [RFC access-control/manager/proxmox{, -yew-comp, -datacenter-manager}/xtermjs 00/11] add remote node shell Fabian Grünbichler
@ 2025-11-05 14:13 ` Fabian Grünbichler
2025-11-05 14:13 ` [pve-devel] [PATCH access-control 1/1] api: ticket: allow token-owned VNC ticket verification Fabian Grünbichler
` (9 subsequent siblings)
10 siblings, 0 replies; 12+ messages in thread
From: Fabian Grünbichler @ 2025-11-05 14:13 UTC (permalink / raw)
To: pve-devel
if a remote name and type is specified, adapt the API endpoint base url
accordingly, and do not send the authentication line, since there is no PDM
termproxy that handles it. instead, PDM will generate and inject the
authentication line when proxying the websocket connection to the termproxy on
the remote.
Signed-off-by: Fabian Grünbichler <f.gruenbichler@proxmox.com>
---
xterm.js/src/main.js | 13 +++++++++++--
1 file changed, 11 insertions(+), 2 deletions(-)
diff --git a/xterm.js/src/main.js b/xterm.js/src/main.js
index 902a1c3..897540f 100644
--- a/xterm.js/src/main.js
+++ b/xterm.js/src/main.js
@@ -19,6 +19,8 @@ var term,
state = states.start,
starttime = new Date();
+var remote = getQueryParameter('remote');
+var remote_type = getQueryParameter('remote-type');
var type = getQueryParameter('console');
var vmid = getQueryParameter('vmid');
var vmname = getQueryParameter('vmname');
@@ -175,7 +177,11 @@ function createTerminal() {
protocol = (location.protocol === 'https:') ? 'wss://' : 'ws://';
var params = {};
- var url = '/nodes/' + nodename;
+ var url = '';
+ if (remote !== undefined && remote !== "") {
+ url += '/' + remote_type + '/remotes/' + remote;
+ }
+ url += '/nodes/' + nodename;
switch (type) {
case 'kvm':
url += '/qemu/' + vmid;
@@ -252,7 +258,10 @@ function runTerminal() {
}, 250);
});
- socket.send(PVE.UserName + ':' + ticket + "\n");
+ // for remote sessions, this line needs to be sent by PDM
+ if (remote === undefined || remote === "") {
+ socket.send(PVE.UserName + ':' + ticket + "\n");
+ }
}
function getLxcStatus(callback) {
--
2.47.3
_______________________________________________
pve-devel mailing list
pve-devel@lists.proxmox.com
https://lists.proxmox.com/cgi-bin/mailman/listinfo/pve-devel
^ permalink raw reply [flat|nested] 12+ messages in thread* [pve-devel] [PATCH access-control 1/1] api: ticket: allow token-owned VNC ticket verification
2025-11-05 14:13 [pve-devel] [RFC access-control/manager/proxmox{, -yew-comp, -datacenter-manager}/xtermjs 00/11] add remote node shell Fabian Grünbichler
2025-11-05 14:13 ` [pve-devel] [PATCH pve-xtermjs 1/1] xtermjs: add support for remote node shells via PDM Fabian Grünbichler
@ 2025-11-05 14:13 ` Fabian Grünbichler
2025-11-05 14:13 ` [pve-devel] [PATCH manager 1/2] api: termproxy/vncwebsocket: allow tokens Fabian Grünbichler
` (8 subsequent siblings)
10 siblings, 0 replies; 12+ messages in thread
From: Fabian Grünbichler @ 2025-11-05 14:13 UTC (permalink / raw)
To: pve-devel
our termproxy will issue a call to this endpoint on pvedaemon to verify the VNC
ticket passed by a client. with PDM, the shell client is actually using a PVE
token to authenticate, and the VNC ticket is owned by this token as well.
Signed-off-by: Fabian Grünbichler <f.gruenbichler@proxmox.com>
---
Notes:
best viewed with -w
alternatively, we could change termproxy to do this directly in
Rust instead of doing an API call..
src/PVE/API2/AccessControl.pm | 40 +++++++++++++++++++++++------------
1 file changed, 27 insertions(+), 13 deletions(-)
diff --git a/src/PVE/API2/AccessControl.pm b/src/PVE/API2/AccessControl.pm
index 457a0a6..a22c700 100644
--- a/src/PVE/API2/AccessControl.pm
+++ b/src/PVE/API2/AccessControl.pm
@@ -274,31 +274,45 @@ __PACKAGE__->register_method({
my $username = $param->{username};
$username .= "\@$param->{realm}" if $param->{realm};
- $username = PVE::AccessControl::lookup_username($username);
+ my $token_vnc_ticket_only = PVE::AccessControl::pve_verify_tokenid($username, 1) && $param->{path} && $param->{privs};
+
+ $username = PVE::AccessControl::lookup_username($username) if !$token_vnc_ticket_only;
my $rpcenv = PVE::RPCEnvironment::get();
my $res;
eval {
- # test if user exists and is enabled
- $rpcenv->check_user_enabled($username);
-
- if ($param->{path} && $param->{privs}) {
+ # special case VNC ticket check by termproxy
+ if ($token_vnc_ticket_only) {
$res = verify_auth(
$rpcenv,
$username,
$param->{password},
- $param->{otp},
+ undef,
$param->{path},
$param->{privs},
);
} else {
- $res = create_ticket_do(
- $rpcenv,
- $username,
- $param->{password},
- $param->{otp},
- $param->{'tfa-challenge'},
- );
+ # test if user exists and is enabled
+ $rpcenv->check_user_enabled($username);
+
+ if ($param->{path} && $param->{privs}) {
+ $res = verify_auth(
+ $rpcenv,
+ $username,
+ $param->{password},
+ $param->{otp},
+ $param->{path},
+ $param->{privs},
+ );
+ } else {
+ $res = create_ticket_do(
+ $rpcenv,
+ $username,
+ $param->{password},
+ $param->{otp},
+ $param->{'tfa-challenge'},
+ );
+ }
}
};
if (my $err = $@) {
--
2.47.3
_______________________________________________
pve-devel mailing list
pve-devel@lists.proxmox.com
https://lists.proxmox.com/cgi-bin/mailman/listinfo/pve-devel
^ permalink raw reply [flat|nested] 12+ messages in thread* [pve-devel] [PATCH manager 1/2] api: termproxy/vncwebsocket: allow tokens
2025-11-05 14:13 [pve-devel] [RFC access-control/manager/proxmox{, -yew-comp, -datacenter-manager}/xtermjs 00/11] add remote node shell Fabian Grünbichler
2025-11-05 14:13 ` [pve-devel] [PATCH pve-xtermjs 1/1] xtermjs: add support for remote node shells via PDM Fabian Grünbichler
2025-11-05 14:13 ` [pve-devel] [PATCH access-control 1/1] api: ticket: allow token-owned VNC ticket verification Fabian Grünbichler
@ 2025-11-05 14:13 ` Fabian Grünbichler
2025-11-05 14:13 ` [pve-devel] [PATCH manager 2/2] api: termproxy: add description to return schema Fabian Grünbichler
` (7 subsequent siblings)
10 siblings, 0 replies; 12+ messages in thread
From: Fabian Grünbichler @ 2025-11-05 14:13 UTC (permalink / raw)
To: pve-devel
this is required for PDM to access a PVE node shell, since the PVE client uses
tokens for authentication. the user still needs a local PAM login to use the shell.
Signed-off-by: Fabian Grünbichler <f.gruenbichler@proxmox.com>
---
PVE/API2/Nodes.pm | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/PVE/API2/Nodes.pm b/PVE/API2/Nodes.pm
index 4590b6186..b29d10f98 100644
--- a/PVE/API2/Nodes.pm
+++ b/PVE/API2/Nodes.pm
@@ -1338,7 +1338,7 @@ __PACKAGE__->register_method({
my ($param) = @_;
my $rpcenv = PVE::RPCEnvironment::get();
- my ($user, undef, $realm) = PVE::AccessControl::verify_username($rpcenv->get_user());
+ my $user = $rpcenv->get_user();
my $node = $param->{node};
my $authpath = "/nodes/$node";
@@ -1410,7 +1410,7 @@ __PACKAGE__->register_method({
my $rpcenv = PVE::RPCEnvironment::get();
- my ($user, undef, $realm) = PVE::AccessControl::verify_username($rpcenv->get_user());
+ my $user = $rpcenv->get_user();
my $authpath = "/nodes/$param->{node}";
--
2.47.3
_______________________________________________
pve-devel mailing list
pve-devel@lists.proxmox.com
https://lists.proxmox.com/cgi-bin/mailman/listinfo/pve-devel
^ permalink raw reply [flat|nested] 12+ messages in thread* [pve-devel] [PATCH manager 2/2] api: termproxy: add description to return schema
2025-11-05 14:13 [pve-devel] [RFC access-control/manager/proxmox{, -yew-comp, -datacenter-manager}/xtermjs 00/11] add remote node shell Fabian Grünbichler
` (2 preceding siblings ...)
2025-11-05 14:13 ` [pve-devel] [PATCH manager 1/2] api: termproxy/vncwebsocket: allow tokens Fabian Grünbichler
@ 2025-11-05 14:13 ` Fabian Grünbichler
2025-11-05 14:13 ` [pve-devel] [PATCH proxmox 1/2] pve-api-types: add termproxy call and types Fabian Grünbichler
` (6 subsequent siblings)
10 siblings, 0 replies; 12+ messages in thread
From: Fabian Grünbichler @ 2025-11-05 14:13 UTC (permalink / raw)
To: pve-devel
needed to call this endpoint from PDM.
Signed-off-by: Fabian Grünbichler <f.gruenbichler@proxmox.com>
---
PVE/API2/Nodes.pm | 11 +++++++----
1 file changed, 7 insertions(+), 4 deletions(-)
diff --git a/PVE/API2/Nodes.pm b/PVE/API2/Nodes.pm
index b29d10f98..9ab8c01e3 100644
--- a/PVE/API2/Nodes.pm
+++ b/PVE/API2/Nodes.pm
@@ -1328,10 +1328,13 @@ __PACKAGE__->register_method({
returns => {
additionalProperties => 0,
properties => {
- user => { type => 'string' },
- ticket => { type => 'string' },
- port => { type => 'integer' },
- upid => { type => 'string' },
+ user => {
+ type => 'string',
+ description => 'user/token that generated the VNC ticket in `ticket`',
+ },
+ ticket => { type => 'string', description => 'VNC ticket used to verifiy websocket connection' },
+ port => { type => 'integer', description => 'port used to bind termproxy to' },
+ upid => { type => 'string', description => 'UPID for termproxy worker task' },
},
},
code => sub {
--
2.47.3
_______________________________________________
pve-devel mailing list
pve-devel@lists.proxmox.com
https://lists.proxmox.com/cgi-bin/mailman/listinfo/pve-devel
^ permalink raw reply [flat|nested] 12+ messages in thread* [pve-devel] [PATCH proxmox 1/2] pve-api-types: add termproxy call and types
2025-11-05 14:13 [pve-devel] [RFC access-control/manager/proxmox{, -yew-comp, -datacenter-manager}/xtermjs 00/11] add remote node shell Fabian Grünbichler
` (3 preceding siblings ...)
2025-11-05 14:13 ` [pve-devel] [PATCH manager 2/2] api: termproxy: add description to return schema Fabian Grünbichler
@ 2025-11-05 14:13 ` Fabian Grünbichler
2025-11-05 14:13 ` [pve-devel] [PATCH proxmox 2/2] http: websocket: add proxy helper Fabian Grünbichler
` (5 subsequent siblings)
10 siblings, 0 replies; 12+ messages in thread
From: Fabian Grünbichler @ 2025-11-05 14:13 UTC (permalink / raw)
To: pve-devel
Signed-off-by: Fabian Grünbichler <f.gruenbichler@proxmox.com>
---
pve-api-types/generate.pl | 2 +
pve-api-types/pve-api.json | 4 ++
pve-api-types/src/generated/code.rs | 20 +++++++-
pve-api-types/src/generated/types.rs | 76 ++++++++++++++++++++++++++++
4 files changed, 101 insertions(+), 1 deletion(-)
diff --git a/pve-api-types/generate.pl b/pve-api-types/generate.pl
index 099437f3..6eecba6d 100644
--- a/pve-api-types/generate.pl
+++ b/pve-api-types/generate.pl
@@ -387,6 +387,8 @@ api(DELETE => '/cluster/sdn/lock', 'release_sdn_lock', 'param-name' => 'ReleaseS
api(PUT => '/cluster/sdn', 'sdn_apply', 'param-name' => 'ReloadSdn', 'output-type' => 'PveUpid');
api(POST => '/cluster/sdn/rollback', 'rollback_sdn_changes', 'param-name' => 'RollbackSdn');
+api(POST => '/nodes/{node}/termproxy', 'node_shell_termproxy', 'param-name' => 'NodeShellTermproxy', 'return-name' => 'NodeShellTicket');
+
# NOW DUMP THE CODE:
#
# We generate one file for API types, and one for API method calls.
diff --git a/pve-api-types/pve-api.json b/pve-api-types/pve-api.json
index 7bde484e..1775fdae 100644
--- a/pve-api-types/pve-api.json
+++ b/pve-api-types/pve-api.json
@@ -51649,15 +51649,19 @@
"additionalProperties": 0,
"properties": {
"port": {
+ "description": "port used to bind termproxy to",
"type": "integer"
},
"ticket": {
+ "description": "VNC ticket used to verifiy websocket connection",
"type": "string"
},
"upid": {
+ "description": "UPID for termproxy worker task",
"type": "string"
},
"user": {
+ "description": "user/token that generated the VNC ticket in `ticket`",
"type": "string"
}
}
diff --git a/pve-api-types/src/generated/code.rs b/pve-api-types/src/generated/code.rs
index dd90ec00..07728f3f 100644
--- a/pve-api-types/src/generated/code.rs
+++ b/pve-api-types/src/generated/code.rs
@@ -356,7 +356,6 @@
/// - /nodes/{node}/storage/{storage}/upload
/// - /nodes/{node}/suspendall
/// - /nodes/{node}/syslog
-/// - /nodes/{node}/termproxy
/// - /nodes/{node}/time
/// - /nodes/{node}/version
/// - /nodes/{node}/vncshell
@@ -585,6 +584,15 @@ pub trait PveClient {
Err(Error::Other("migrate_qemu not implemented"))
}
+ /// Creates a VNC Shell proxy.
+ async fn node_shell_termproxy(
+ &self,
+ node: &str,
+ params: NodeShellTermproxy,
+ ) -> Result<NodeShellTicket, Error> {
+ Err(Error::Other("node_shell_termproxy not implemented"))
+ }
+
/// Read node status
async fn node_status(&self, node: &str) -> Result<NodeStatus, Error> {
Err(Error::Other("node_status not implemented"))
@@ -1116,6 +1124,16 @@ where
Ok(self.0.post(url, ¶ms).await?.expect_json()?.data)
}
+ /// Creates a VNC Shell proxy.
+ async fn node_shell_termproxy(
+ &self,
+ node: &str,
+ params: NodeShellTermproxy,
+ ) -> Result<NodeShellTicket, Error> {
+ let url = &format!("/api2/extjs/nodes/{node}/termproxy");
+ Ok(self.0.post(url, ¶ms).await?.expect_json()?.data)
+ }
+
/// Read node status
async fn node_status(&self, node: &str) -> Result<NodeStatus, Error> {
let url = &format!("/api2/extjs/nodes/{node}/status");
diff --git a/pve-api-types/src/generated/types.rs b/pve-api-types/src/generated/types.rs
index 5321cf67..6c42b620 100644
--- a/pve-api-types/src/generated/types.rs
+++ b/pve-api-types/src/generated/types.rs
@@ -4223,6 +4223,82 @@ pub enum NetworkInterfaceVlanProtocol {
serde_plain::derive_display_from_serialize!(NetworkInterfaceVlanProtocol);
serde_plain::derive_fromstr_from_deserialize!(NetworkInterfaceVlanProtocol);
+#[api(
+ properties: {
+ cmd: {
+ optional: true,
+ type: NodeShellTermproxyCmd,
+ },
+ "cmd-opts": {
+ default: "",
+ optional: true,
+ type: String,
+ },
+ },
+)]
+/// Object.
+#[derive(Debug, serde::Deserialize, serde::Serialize)]
+pub struct NodeShellTermproxy {
+ #[serde(default, skip_serializing_if = "Option::is_none")]
+ pub cmd: Option<NodeShellTermproxyCmd>,
+
+ /// Add parameters to a command. Encoded as null terminated strings.
+ #[serde(default, skip_serializing_if = "Option::is_none")]
+ #[serde(rename = "cmd-opts")]
+ pub cmd_opts: Option<String>,
+}
+
+#[api]
+/// Run specific command or default to login (requires 'root@pam')
+#[derive(Clone, Copy, Debug, Default, Eq, PartialEq, serde::Deserialize, serde::Serialize)]
+pub enum NodeShellTermproxyCmd {
+ #[serde(rename = "ceph_install")]
+ /// ceph_install.
+ CephInstall,
+ #[serde(rename = "login")]
+ #[default]
+ /// login.
+ Login,
+ #[serde(rename = "upgrade")]
+ /// upgrade.
+ Upgrade,
+}
+serde_plain::derive_display_from_serialize!(NodeShellTermproxyCmd);
+serde_plain::derive_fromstr_from_deserialize!(NodeShellTermproxyCmd);
+
+#[api(
+ properties: {
+ port: {
+ type: Integer,
+ },
+ ticket: {
+ type: String,
+ },
+ upid: {
+ type: String,
+ },
+ user: {
+ type: String,
+ },
+ },
+)]
+/// Object.
+#[derive(Debug, serde::Deserialize, serde::Serialize)]
+pub struct NodeShellTicket {
+ /// port used to bind termproxy to
+ #[serde(deserialize_with = "proxmox_serde::perl::deserialize_i64")]
+ pub port: i64,
+
+ /// ticket used to verifiy websocket connection
+ pub ticket: String,
+
+ /// UPID for termproxy worker task
+ pub upid: String,
+
+ /// user
+ pub user: String,
+}
+
#[api(
additional_properties: "additional_properties",
properties: {
--
2.47.3
_______________________________________________
pve-devel mailing list
pve-devel@lists.proxmox.com
https://lists.proxmox.com/cgi-bin/mailman/listinfo/pve-devel
^ permalink raw reply [flat|nested] 12+ messages in thread* [pve-devel] [PATCH proxmox 2/2] http: websocket: add proxy helper
2025-11-05 14:13 [pve-devel] [RFC access-control/manager/proxmox{, -yew-comp, -datacenter-manager}/xtermjs 00/11] add remote node shell Fabian Grünbichler
` (4 preceding siblings ...)
2025-11-05 14:13 ` [pve-devel] [PATCH proxmox 1/2] pve-api-types: add termproxy call and types Fabian Grünbichler
@ 2025-11-05 14:13 ` Fabian Grünbichler
2025-11-05 14:13 ` [pve-devel] [PATCH proxmox-yew-comp 1/1] xtermjs: add remote support Fabian Grünbichler
` (4 subsequent siblings)
10 siblings, 0 replies; 12+ messages in thread
From: Fabian Grünbichler @ 2025-11-05 14:13 UTC (permalink / raw)
To: pve-devel
allows wiring up two websocket connections so that decoded data from upstream
is sent forward downstream encoded as websocket frames, and vice versa, all
while handling control frames.
the preamble is used to inject an authentication line for xtermjs/termproxy by
PDM.
Signed-off-by: Fabian Grünbichler <f.gruenbichler@proxmox.com>
---
proxmox-http/src/websocket/mod.rs | 67 +++++++++++++++++++++++++++++++
1 file changed, 67 insertions(+)
diff --git a/proxmox-http/src/websocket/mod.rs b/proxmox-http/src/websocket/mod.rs
index eb808b52..4bf47eb2 100644
--- a/proxmox-http/src/websocket/mod.rs
+++ b/proxmox-http/src/websocket/mod.rs
@@ -821,4 +821,71 @@ impl WebSocket {
}
}
}
+
+ /// Takes two websocket endpoints and connects them by re-encoding the data.
+ ///
+ /// This method takes care of copying the data between endpoints, and sending correct responses
+ /// for control frames (e.g. a Point to a Ping).
+ ///
+ /// The `preamble` allows injecting initial handshake data into the proxying.
+ pub async fn proxy_connection<S, L>(
+ &self,
+ upstream: S,
+ downstream: L,
+ preamble: &[u8],
+ ) -> Result<(), Error>
+ where
+ S: AsyncRead + AsyncWrite + Unpin + Send + 'static,
+ L: AsyncRead + AsyncWrite + Unpin + Send + 'static,
+ {
+ // unmasked as the spec requires
+ let server_socket = WebSocket { mask: None };
+
+ // split to allow duplex transfer
+ let (upstream_raw_reader, upstream_raw_writer) = tokio::io::split(upstream);
+ let (downstream_raw_reader, downstream_raw_writer) = tokio::io::split(downstream);
+
+ // wire up WS handling for upstream connection
+ let (upstream_control_tx, mut upstream_control_rx) = mpsc::unbounded_channel();
+ let mut upstream_ws_reader = WebSocketReader::new(upstream_raw_reader, upstream_control_tx);
+ let mut upstream_ws_writer = WebSocketWriter::new(server_socket.mask, upstream_raw_writer);
+
+ // wire up WS handling for downstream connection
+ let (downstream_control_tx, mut downstream_control_rx) = mpsc::unbounded_channel();
+ let mut downstream_ws_reader =
+ WebSocketReader::new(downstream_raw_reader, downstream_control_tx);
+ let mut downstream_ws_writer = WebSocketWriter::new(self.mask, downstream_raw_writer);
+
+ // send preamble downstream via WS
+ if !preamble.is_empty() {
+ downstream_ws_writer.write_all(preamble).await?;
+ }
+
+ // read from upstream, write to downstream while handling control frames received from
+ // downstream
+ let downstream_future = server_socket.copy_to_websocket(
+ &mut upstream_ws_reader,
+ &mut downstream_ws_writer,
+ &mut downstream_control_rx,
+ );
+
+ // read from downstream, write to upstream while handling control frames received from
+ // upstream
+ let upstream_future = self.copy_to_websocket(
+ &mut downstream_ws_reader,
+ &mut upstream_ws_writer,
+ &mut upstream_control_rx,
+ );
+
+ select! {
+ res = downstream_future.fuse() => match res {
+ Ok(_) => Ok(()),
+ Err(err) => Err(Error::from(err)),
+ },
+ res = upstream_future.fuse() => match res {
+ Ok(_) => Ok(()),
+ Err(err) => Err(Error::from(err)),
+ },
+ }
+ }
}
--
2.47.3
_______________________________________________
pve-devel mailing list
pve-devel@lists.proxmox.com
https://lists.proxmox.com/cgi-bin/mailman/listinfo/pve-devel
^ permalink raw reply [flat|nested] 12+ messages in thread* [pve-devel] [PATCH proxmox-yew-comp 1/1] xtermjs: add remote support
2025-11-05 14:13 [pve-devel] [RFC access-control/manager/proxmox{, -yew-comp, -datacenter-manager}/xtermjs 00/11] add remote node shell Fabian Grünbichler
` (5 preceding siblings ...)
2025-11-05 14:13 ` [pve-devel] [PATCH proxmox 2/2] http: websocket: add proxy helper Fabian Grünbichler
@ 2025-11-05 14:13 ` Fabian Grünbichler
2025-11-05 14:13 ` [pve-devel] [PATCH proxmox-datacenter-manager 1/4] connection: add access to "raw" client Fabian Grünbichler
` (3 subsequent siblings)
10 siblings, 0 replies; 12+ messages in thread
From: Fabian Grünbichler @ 2025-11-05 14:13 UTC (permalink / raw)
To: pve-devel
by defining a new ConsoleType and adding a remote_name property/parameter.
Signed-off-by: Fabian Grünbichler <f.gruenbichler@proxmox.com>
---
src/apt_package_manager.rs | 9 ++++++---
src/xtermjs.rs | 38 +++++++++++++++++++++++++++++++++-----
2 files changed, 39 insertions(+), 8 deletions(-)
diff --git a/src/apt_package_manager.rs b/src/apt_package_manager.rs
index 2e2d01a..0b392be 100644
--- a/src/apt_package_manager.rs
+++ b/src/apt_package_manager.rs
@@ -202,9 +202,12 @@ impl LoadableComponent for ProxmoxAptPackageManager {
let on_upgrade = props.on_upgrade.clone();
let on_upgrade = move |_| match &on_upgrade {
Some(on_upgrade) => on_upgrade.emit(()),
- None => {
- XTermJs::open_xterm_js_viewer(crate::ConsoleType::UpgradeShell, "localhost", false)
- }
+ None => XTermJs::open_xterm_js_viewer(
+ crate::ConsoleType::UpgradeShell,
+ None,
+ "localhost",
+ false,
+ ),
};
let toolbar = Toolbar::new()
diff --git a/src/xtermjs.rs b/src/xtermjs.rs
index 4eb464d..36b5379 100644
--- a/src/xtermjs.rs
+++ b/src/xtermjs.rs
@@ -17,6 +17,11 @@ pub struct XTermJs {
#[prop_or_default]
pub key: Option<Key>,
+ #[prop_or_default]
+ /// The remote name, if this is a remote node shell
+ #[builder(IntoPropValue, into_prop_value)]
+ pub remote_name: Option<AttrValue>,
+
#[prop_or("localhost".into())]
#[builder(IntoPropValue, into_prop_value)]
/// The node name.
@@ -46,8 +51,13 @@ impl XTermJs {
// FIXME: separate noVNC and xterm.js, this is not a nice interface!
/// Open a new terminal window.
- pub fn open_xterm_js_viewer(console_type: ConsoleType, node_name: &str, vnc: bool) {
- let url = xtermjs_url(console_type, node_name, vnc);
+ pub fn open_xterm_js_viewer(
+ console_type: ConsoleType,
+ remote_name: Option<&str>,
+ node_name: &str,
+ vnc: bool,
+ ) {
+ let url = xtermjs_url(console_type, remote_name, node_name, vnc);
let target = "_blank";
let features =
"toolbar=no,location=no,status=no,menubar=no,resizable=yes,width=800,height=420";
@@ -72,14 +82,20 @@ pub enum ConsoleType {
LXC(u64),
UpgradeShell,
LoginShell,
+ RemotePveLoginShell,
}
-fn xtermjs_url(console_type: ConsoleType, node_name: &str, vnc: bool) -> String {
+fn xtermjs_url(
+ console_type: ConsoleType,
+ remote_name: Option<&str>,
+ node_name: &str,
+ vnc: bool,
+) -> String {
let console = match console_type {
ConsoleType::KVM(_vmid) => "kvm",
ConsoleType::LXC(_vmid) => "lxc",
ConsoleType::UpgradeShell => "upgrade",
- ConsoleType::LoginShell => "shell",
+ ConsoleType::LoginShell | ConsoleType::RemotePveLoginShell => "shell",
};
let mut param = json!({
@@ -105,6 +121,13 @@ fn xtermjs_url(console_type: ConsoleType, node_name: &str, vnc: bool) -> String
ConsoleType::LoginShell => {
param["cmd"] = "login".into();
}
+ ConsoleType::RemotePveLoginShell => {
+ param["cmd"] = "login".into();
+ param["remote-type"] = "pve".into();
+ param["remote"] = remote_name
+ .expect("RemotePveLoginShell requires remote name")
+ .into();
+ }
}
format!("?{}", json_object_to_query(param).unwrap())
@@ -122,7 +145,12 @@ impl Component for ProxmoxXTermJs {
fn view(&self, ctx: &Context<Self>) -> Html {
let props = ctx.props();
- let url = xtermjs_url(props.console_type, &props.node_name, props.vnc);
+ let url = xtermjs_url(
+ props.console_type,
+ props.remote_name.as_deref(),
+ &props.node_name,
+ props.vnc,
+ );
html! {<iframe class="pwt-flex-fit" src={format!("/{url}")}/>}
}
}
--
2.47.3
_______________________________________________
pve-devel mailing list
pve-devel@lists.proxmox.com
https://lists.proxmox.com/cgi-bin/mailman/listinfo/pve-devel
^ permalink raw reply [flat|nested] 12+ messages in thread* [pve-devel] [PATCH proxmox-datacenter-manager 1/4] connection: add access to "raw" client
2025-11-05 14:13 [pve-devel] [RFC access-control/manager/proxmox{, -yew-comp, -datacenter-manager}/xtermjs 00/11] add remote node shell Fabian Grünbichler
` (6 preceding siblings ...)
2025-11-05 14:13 ` [pve-devel] [PATCH proxmox-yew-comp 1/1] xtermjs: add remote support Fabian Grünbichler
@ 2025-11-05 14:13 ` Fabian Grünbichler
2025-11-05 14:13 ` [pve-devel] [PATCH proxmox-datacenter-manager 2/4] api: pve: add termproxy endpoint Fabian Grünbichler
` (2 subsequent siblings)
10 siblings, 0 replies; 12+ messages in thread
From: Fabian Grünbichler @ 2025-11-05 14:13 UTC (permalink / raw)
To: pve-devel
needed for websocket connections and similar lower level access.
Signed-off-by: Fabian Grünbichler <f.gruenbichler@proxmox.com>
---
server/src/connection.rs | 11 +++++++++++
server/src/metric_collection/collection_task.rs | 5 +++++
server/src/test_support/fake_remote.rs | 5 +++++
3 files changed, 21 insertions(+)
diff --git a/server/src/connection.rs b/server/src/connection.rs
index e749c1a..1c0069e 100644
--- a/server/src/connection.rs
+++ b/server/src/connection.rs
@@ -252,6 +252,9 @@ pub trait ClientFactory {
///
/// Note: currently does not support two factor authentication.
async fn make_pbs_client_and_login(&self, remote: &Remote) -> Result<Box<PbsClient>, Error>;
+
+ /// Create a new API client for raw acess to the given remote
+ fn make_raw_client(&self, remote: &Remote) -> Result<Box<Client>, Error>;
}
/// Default production client factory
@@ -346,6 +349,10 @@ impl ClientFactory for DefaultClientFactory {
ConnectionCache::get().make_pve_client(remote)
}
+ fn make_raw_client(&self, remote: &Remote) -> Result<Box<Client>, Error> {
+ Ok(Box::new(crate::connection::connect(remote, None)?))
+ }
+
fn make_pbs_client(&self, remote: &Remote) -> Result<Box<PbsClient>, Error> {
let client = crate::connection::connect(remote, None)?;
Ok(Box::new(PbsClient(client)))
@@ -418,6 +425,10 @@ pub fn make_pbs_client(remote: &Remote) -> Result<Box<PbsClient>, Error> {
instance().make_pbs_client(remote)
}
+pub fn make_raw_client(remote: &Remote) -> Result<Box<Client>, Error> {
+ instance().make_raw_client(remote)
+}
+
/// Create a new API client for PVE remotes.
///
/// In case the remote has a user configured (instead of an API token), it will connect and get a
diff --git a/server/src/metric_collection/collection_task.rs b/server/src/metric_collection/collection_task.rs
index a6c8443..cc1a460 100644
--- a/server/src/metric_collection/collection_task.rs
+++ b/server/src/metric_collection/collection_task.rs
@@ -387,6 +387,7 @@ pub(super) mod tests {
use http::StatusCode;
use pdm_api_types::Authid;
+ use proxmox_client::Client;
use pve_api_types::{ClusterMetrics, ClusterMetricsData};
use crate::{
@@ -430,6 +431,10 @@ pub(super) mod tests {
bail!("not implemented")
}
+ fn make_raw_client(&self, _remote: &Remote) -> Result<Box<Client>, Error> {
+ bail!("not implemented")
+ }
+
async fn make_pve_client_and_login(
&self,
_remote: &Remote,
diff --git a/server/src/test_support/fake_remote.rs b/server/src/test_support/fake_remote.rs
index cd2ccf6..62dd869 100644
--- a/server/src/test_support/fake_remote.rs
+++ b/server/src/test_support/fake_remote.rs
@@ -5,6 +5,7 @@ use serde::Deserialize;
use pdm_api_types::{remotes::Remote, Authid, ConfigDigest};
use pdm_config::remotes::RemoteConfig;
+use proxmox_client::Client;
use proxmox_product_config::ApiLockGuard;
use proxmox_section_config::typed::SectionConfigData;
use pve_api_types::{
@@ -100,6 +101,10 @@ impl ClientFactory for FakeClientFactory {
bail!("not implemented")
}
+ fn make_raw_client(&self, _remote: &Remote) -> Result<Box<Client>, Error> {
+ bail!("not implemented")
+ }
+
async fn make_pve_client_and_login(&self, _remote: &Remote) -> Result<Arc<PveClient>, Error> {
bail!("not implemented")
}
--
2.47.3
_______________________________________________
pve-devel mailing list
pve-devel@lists.proxmox.com
https://lists.proxmox.com/cgi-bin/mailman/listinfo/pve-devel
^ permalink raw reply [flat|nested] 12+ messages in thread* [pve-devel] [PATCH proxmox-datacenter-manager 2/4] api: pve: add termproxy endpoint
2025-11-05 14:13 [pve-devel] [RFC access-control/manager/proxmox{, -yew-comp, -datacenter-manager}/xtermjs 00/11] add remote node shell Fabian Grünbichler
` (7 preceding siblings ...)
2025-11-05 14:13 ` [pve-devel] [PATCH proxmox-datacenter-manager 1/4] connection: add access to "raw" client Fabian Grünbichler
@ 2025-11-05 14:13 ` Fabian Grünbichler
2025-11-05 14:13 ` [pve-devel] [PATCH proxmox-datacenter-manager 3/4] api: pve: add vncwebsocket endpoint Fabian Grünbichler
2025-11-05 14:13 ` [pve-devel] [PATCH proxmox-datacenter-manager 4/4] ui: pve: node: add shell tab Fabian Grünbichler
10 siblings, 0 replies; 12+ messages in thread
From: Fabian Grünbichler @ 2025-11-05 14:13 UTC (permalink / raw)
To: pve-devel
contrary to the PDM node one or the PVE/PBS ones, this doesn't spawn a
termproxy instance, but just generates the ticket used for authenticating the
websocket upgrade on the PDM side.
the returned port is hard-coded as 0 to be compatible with the rest of the
stack that expects one.
Signed-off-by: Fabian Grünbichler <f.gruenbichler@proxmox.com>
---
server/src/api/pve/node.rs | 83 ++++++++++++++++++++++++++++++++++++--
1 file changed, 80 insertions(+), 3 deletions(-)
diff --git a/server/src/api/pve/node.rs b/server/src/api/pve/node.rs
index 301c0b1..a1a784e 100644
--- a/server/src/api/pve/node.rs
+++ b/server/src/api/pve/node.rs
@@ -1,10 +1,17 @@
-use anyhow::Error;
+use anyhow::{bail, format_err, Error};
+use serde_json::{json, Value};
-use proxmox_router::{list_subdirs_api_method, Permission, Router, SubdirMap};
+use proxmox_auth_api::{
+ ticket::{Empty, Ticket},
+ Keyring,
+};
+use proxmox_router::{list_subdirs_api_method, Permission, Router, RpcEnvironment, SubdirMap};
use proxmox_schema::api;
use proxmox_sortable_macro::sortable;
-use pdm_api_types::{remotes::REMOTE_ID_SCHEMA, NODE_SCHEMA, PRIV_RESOURCE_AUDIT};
+use pdm_api_types::{
+ remotes::REMOTE_ID_SCHEMA, Authid, NODE_SCHEMA, PRIV_RESOURCE_AUDIT, PRIV_SYS_CONSOLE,
+};
use pve_api_types::StorageContent;
use crate::api::pve::storage;
@@ -18,6 +25,7 @@ const SUBDIRS: SubdirMap = &sorted!([
("apt", &crate::api::remote_updates::APT_ROUTER),
("rrddata", &super::rrddata::NODE_RRD_ROUTER),
("network", &Router::new().get(&API_METHOD_GET_NETWORK)),
+ ("termproxy", &Router::new().post(&API_METHOD_SHELL_TICKET)),
("storage", &STORAGE_ROUTER),
("status", &Router::new().get(&API_METHOD_GET_STATUS)),
]);
@@ -129,3 +137,72 @@ async fn get_status(remote: String, node: String) -> Result<pve_api_types::NodeS
let result = client.node_status(&node).await?;
Ok(result)
}
+
+fn encode_term_ticket_path(remote: &str, node: &str) -> String {
+ format!("/shell/{remote}/{node}")
+}
+
+#[api(
+ protected: true,
+ input: {
+ properties: {
+ remote: { schema: REMOTE_ID_SCHEMA },
+ node: {
+ schema: NODE_SCHEMA,
+ },
+ },
+ },
+ returns: {
+ type: Object,
+ description: "Object with the user and ticket",
+ properties: {
+ user: {
+ description: "User that obtained the VNC ticket.",
+ type: String,
+ },
+ ticket: {
+ description: "VNC ticket used to authenticate websocket upgrade.",
+ type: String,
+ },
+ port: {
+ description: "Always '0'.",
+ type: Integer,
+ }
+ }
+ },
+ access: {
+ description: "Restricted to users",
+ permission: &Permission::Privilege(&["resource", "{remote}", "node", "{node}"], PRIV_SYS_CONSOLE, false),
+ }
+)]
+/// Call termproxy and return shell ticket
+async fn shell_ticket(
+ remote: String,
+ node: String,
+ rpcenv: &mut dyn RpcEnvironment,
+) -> Result<Value, Error> {
+ // intentionally user only for now
+ let auth_id: Authid = rpcenv
+ .get_auth_id()
+ .ok_or_else(|| format_err!("no authid available"))?
+ .parse()?;
+
+ if auth_id.is_token() {
+ bail!("API tokens cannot access this API endpoint");
+ }
+
+ let userid = auth_id.user();
+ let path = encode_term_ticket_path(&remote, &node);
+
+ let private_auth_keyring =
+ Keyring::with_private_key(crate::auth::key::private_auth_key().clone());
+
+ let ticket = Ticket::new(crate::auth::TERM_PREFIX, &Empty)?
+ .sign(&private_auth_keyring, Some(&format!("{}{}", userid, path)))?;
+
+ Ok(json!({
+ "user": userid,
+ "ticket": ticket,
+ "port": 0,
+ }))
+}
--
2.47.3
_______________________________________________
pve-devel mailing list
pve-devel@lists.proxmox.com
https://lists.proxmox.com/cgi-bin/mailman/listinfo/pve-devel
^ permalink raw reply [flat|nested] 12+ messages in thread* [pve-devel] [PATCH proxmox-datacenter-manager 3/4] api: pve: add vncwebsocket endpoint
2025-11-05 14:13 [pve-devel] [RFC access-control/manager/proxmox{, -yew-comp, -datacenter-manager}/xtermjs 00/11] add remote node shell Fabian Grünbichler
` (8 preceding siblings ...)
2025-11-05 14:13 ` [pve-devel] [PATCH proxmox-datacenter-manager 2/4] api: pve: add termproxy endpoint Fabian Grünbichler
@ 2025-11-05 14:13 ` Fabian Grünbichler
2025-11-05 14:13 ` [pve-devel] [PATCH proxmox-datacenter-manager 4/4] ui: pve: node: add shell tab Fabian Grünbichler
10 siblings, 0 replies; 12+ messages in thread
From: Fabian Grünbichler @ 2025-11-05 14:13 UTC (permalink / raw)
To: pve-devel
instead of directly forwarding to a running termproxy instance, PDM needs to
authenticate the incoming connection, create a new VNC ticket (and spawn a
termproxy instance) on the remote side, and forward the incoming connection to
the remote.
Signed-off-by: Fabian Grünbichler <f.gruenbichler@proxmox.com>
---
Notes:
some of this code should live in a lower level helper for re-use by the PBS remote..
server/src/api/pve/node.rs | 178 ++++++++++++++++++++++++++++++++++++-
1 file changed, 175 insertions(+), 3 deletions(-)
diff --git a/server/src/api/pve/node.rs b/server/src/api/pve/node.rs
index a1a784e..1937c2a 100644
--- a/server/src/api/pve/node.rs
+++ b/server/src/api/pve/node.rs
@@ -1,12 +1,25 @@
use anyhow::{bail, format_err, Error};
+use futures::{FutureExt, TryFutureExt};
+use http::{
+ header::{SEC_WEBSOCKET_KEY, SEC_WEBSOCKET_VERSION, UPGRADE},
+ request::Parts,
+ Method, Request, StatusCode,
+};
+use hyper::upgrade::Upgraded;
+use hyper_util::rt::TokioIo;
use serde_json::{json, Value};
use proxmox_auth_api::{
ticket::{Empty, Ticket},
Keyring,
};
-use proxmox_router::{list_subdirs_api_method, Permission, Router, RpcEnvironment, SubdirMap};
-use proxmox_schema::api;
+use proxmox_client::ApiPathBuilder;
+use proxmox_http::{websocket::WebSocket, Body};
+use proxmox_router::{
+ list_subdirs_api_method, ApiHandler, ApiMethod, ApiResponseFuture, Permission, Router,
+ RpcEnvironment, SubdirMap,
+};
+use proxmox_schema::{api, IntegerSchema, ObjectSchema, StringSchema};
use proxmox_sortable_macro::sortable;
use pdm_api_types::{
@@ -14,7 +27,7 @@ use pdm_api_types::{
};
use pve_api_types::StorageContent;
-use crate::api::pve::storage;
+use crate::api::{nodes::vncwebsocket::required_string_param, pve::storage};
pub const ROUTER: Router = Router::new()
.get(&list_subdirs_api_method!(SUBDIRS))
@@ -26,6 +39,10 @@ const SUBDIRS: SubdirMap = &sorted!([
("rrddata", &super::rrddata::NODE_RRD_ROUTER),
("network", &Router::new().get(&API_METHOD_GET_NETWORK)),
("termproxy", &Router::new().post(&API_METHOD_SHELL_TICKET)),
+ (
+ "vncwebsocket",
+ &Router::new().upgrade(&API_METHOD_SHELL_WEBSOCKET)
+ ),
("storage", &STORAGE_ROUTER),
("status", &Router::new().get(&API_METHOD_GET_STATUS)),
]);
@@ -206,3 +223,158 @@ async fn shell_ticket(
"port": 0,
}))
}
+
+#[sortable]
+pub const API_METHOD_SHELL_WEBSOCKET: ApiMethod = ApiMethod::new(
+ &ApiHandler::AsyncHttp(&upgrade_to_websocket),
+ &ObjectSchema::new(
+ "Upgraded to websocket",
+ &sorted!([
+ ("remote", false, &REMOTE_ID_SCHEMA),
+ ("node", false, &NODE_SCHEMA),
+ (
+ "vncticket",
+ false,
+ &StringSchema::new("Terminal ticket").schema()
+ ),
+ ("port", false, &IntegerSchema::new("Terminal port").schema()),
+ ]),
+ ),
+)
+.access(
+ Some("The user needs Sys.Console on /resource/{remote}/node/{node}."),
+ &Permission::Privilege(
+ &["resource", "{remote}", "node", "{node}"],
+ PRIV_SYS_CONSOLE,
+ false,
+ ),
+);
+
+fn upgrade_to_websocket(
+ parts: Parts,
+ req_body: hyper::body::Incoming,
+ param: Value,
+ _info: &ApiMethod,
+ rpcenv: Box<dyn RpcEnvironment>,
+) -> ApiResponseFuture {
+ async move {
+ // intentionally user only for now
+ let auth_id: Authid = rpcenv
+ .get_auth_id()
+ .ok_or_else(|| format_err!("no authid available"))?
+ .parse()?;
+
+ if auth_id.is_token() {
+ bail!("API tokens cannot access this API endpoint");
+ }
+
+ let userid = auth_id.user();
+ let ticket = required_string_param(¶m, "vncticket")?;
+
+ let public_auth_keyring =
+ Keyring::with_public_key(crate::auth::key::public_auth_key().clone());
+
+ let remote = required_string_param(¶m, "remote")?.to_owned();
+ let node = required_string_param(¶m, "node")?.to_owned();
+ let ticket_path = encode_term_ticket_path(&remote, &node);
+
+ Ticket::<Empty>::parse(ticket)?.verify(
+ &public_auth_keyring,
+ crate::auth::TERM_PREFIX,
+ Some(&format!("{}{}", userid, ticket_path)),
+ )?;
+
+ let (mut ws, response) = WebSocket::new(parts.headers.clone())?;
+
+ proxmox_rest_server::spawn_internal_task(async move {
+ let incoming_ws: Upgraded =
+ match hyper::upgrade::on(Request::from_parts(parts, req_body))
+ .map_err(Error::from)
+ .await
+ {
+ Ok(upgraded) => upgraded,
+ _ => bail!("error"),
+ };
+
+ let (remotes, _) = pdm_config::remotes::config()?;
+ let pve = super::connect_to_remote(&remotes, &remote)?;
+ let pve_term_ticket = pve
+ .node_shell_termproxy(
+ &node,
+ pve_api_types::NodeShellTermproxy {
+ cmd: None,
+ cmd_opts: None,
+ },
+ )
+ .await?;
+
+ let remote = super::get_remote(&remotes, &remote)?;
+ let raw_client = crate::connection::make_raw_client(remote)?;
+
+ let ws_key = proxmox_sys::linux::random_data(16)?;
+ let ws_key = proxmox_base64::encode(&ws_key);
+
+ let api_url = raw_client.api_url().clone().into_parts();
+
+ let mut builder = http::uri::Builder::new();
+ if let Some(scheme) = api_url.scheme {
+ builder = builder.scheme(scheme);
+ }
+ if let Some(authority) = api_url.authority {
+ builder = builder.authority(authority)
+ }
+ let api_path = ApiPathBuilder::new(format!("/api2/json/nodes/{node}/vncwebsocket"))
+ .arg("vncticket", pve_term_ticket.ticket.clone())
+ .arg("port", pve_term_ticket.port)
+ .build();
+ let uri = builder
+ .path_and_query(api_path)
+ .build()
+ .map_err(|err| format_err!("failed to build Uri - {err}"))?;
+
+ let auth = raw_client.login_auth()?;
+ let req = Request::builder()
+ .method(Method::GET)
+ .uri(uri)
+ .header(UPGRADE, "websocket")
+ .header(SEC_WEBSOCKET_VERSION, "13")
+ .header(SEC_WEBSOCKET_KEY, ws_key);
+
+ let req = auth.set_auth_headers(req).body(Body::empty())?;
+
+ let res = raw_client.http_client().request(req).await?;
+ if res.status() != StatusCode::SWITCHING_PROTOCOLS {
+ bail!("server didn't upgrade: {}", res.status());
+ }
+
+ let pve_ws = hyper::upgrade::on(res)
+ .await
+ .map_err(|err| format_err!("failed to upgrade - {}", err))?;
+
+ let username = if let proxmox_client::AuthenticationKind::Token(ref token) = *auth {
+ token.userid.clone()
+ } else {
+ bail!("shell not supported with ticket-based authentication")
+ };
+
+ let preamble = format!("{username}:{ticket}\n", ticket = pve_term_ticket.ticket);
+ ws.mask = Some([0, 0, 0, 0]);
+
+ if let Err(err) = ws
+ .proxy_connection(
+ TokioIo::new(incoming_ws),
+ TokioIo::new(pve_ws),
+ preamble.as_bytes(),
+ )
+ .await
+ {
+ log::warn!("error while copying between websockets: {err:?}");
+ }
+
+ Ok(())
+ });
+
+ Ok(response)
+ }
+ .boxed()
+}
--
2.47.3
_______________________________________________
pve-devel mailing list
pve-devel@lists.proxmox.com
https://lists.proxmox.com/cgi-bin/mailman/listinfo/pve-devel
^ permalink raw reply [flat|nested] 12+ messages in thread* [pve-devel] [PATCH proxmox-datacenter-manager 4/4] ui: pve: node: add shell tab
2025-11-05 14:13 [pve-devel] [RFC access-control/manager/proxmox{, -yew-comp, -datacenter-manager}/xtermjs 00/11] add remote node shell Fabian Grünbichler
` (9 preceding siblings ...)
2025-11-05 14:13 ` [pve-devel] [PATCH proxmox-datacenter-manager 3/4] api: pve: add vncwebsocket endpoint Fabian Grünbichler
@ 2025-11-05 14:13 ` Fabian Grünbichler
10 siblings, 0 replies; 12+ messages in thread
From: Fabian Grünbichler @ 2025-11-05 14:13 UTC (permalink / raw)
To: pve-devel
Signed-off-by: Fabian Grünbichler <f.gruenbichler@proxmox.com>
---
Notes:
could also spawn a new window instead, which might be more ergonomic ;)
ui/src/pve/node/mod.rs | 19 ++++++++++++++++++-
1 file changed, 18 insertions(+), 1 deletion(-)
diff --git a/ui/src/pve/node/mod.rs b/ui/src/pve/node/mod.rs
index d89bcde..6e8f2d8 100644
--- a/ui/src/pve/node/mod.rs
+++ b/ui/src/pve/node/mod.rs
@@ -1,7 +1,7 @@
use std::rc::Rc;
use gloo_utils::window;
-use proxmox_yew_comp::AptPackageManager;
+use proxmox_yew_comp::{AptPackageManager, ConsoleType, XTermJs};
use yew::{
virtual_dom::{VComp, VNode},
Context,
@@ -113,6 +113,23 @@ impl yew::Component for NodePanelComp {
}
},
)
+ .with_item_builder(
+ TabBarItem::new()
+ .key("shell_view")
+ .label(tr!("Shell"))
+ .icon_class("fa fa-terminal"),
+ {
+ let remote = props.remote.clone();
+ let node = props.node.clone();
+ move |_| {
+ let mut xtermjs = XTermJs::new();
+ xtermjs.set_remote_name(Some(remote.clone()));
+ xtermjs.set_node_name(node.clone());
+ xtermjs.set_console_type(ConsoleType::RemotePveLoginShell);
+ xtermjs.into()
+ }
+ },
+ )
.into()
}
}
--
2.47.3
_______________________________________________
pve-devel mailing list
pve-devel@lists.proxmox.com
https://lists.proxmox.com/cgi-bin/mailman/listinfo/pve-devel
^ permalink raw reply [flat|nested] 12+ messages in thread