From be2f81a24d17bc0c930ac67b032d518e2c3a188d Mon Sep 17 00:00:00 2001 From: Albert Esteve Date: Tue, 9 Dec 2025 13:21:15 +0100 Subject: [PATCH 1/3] feat(driver-vnc): create vnc driver Create a vnc driver for jumpstarter that opens a tunnel to connect vnc clients locally (and opens a browser directly to that tunnel). If a VNC server is running in the remote machine it will allow sharing the screen. Co-Authored-By: Claude noreply@anthropic.com Signed-off-by: Albert Esteve (cherry picked from commit 659fe34bec7c8a83860f3a7efc09364b0341f57a) --- .../reference/package-apis/drivers/index.md | 2 + .../reference/package-apis/drivers/vnc.md | 1 + packages/jumpstarter-driver-vnc/README.md | 58 +++++++++++++++++ .../examples/exporter.yaml | 16 +++++ .../jumpstarter_driver_vnc/__init__.py | 3 + .../jumpstarter_driver_vnc/client.py | 65 +++++++++++++++++++ .../jumpstarter_driver_vnc/driver.py | 19 ++++++ .../jumpstarter_driver_vnc/driver_test.py | 34 ++++++++++ .../jumpstarter-driver-vnc/pyproject.toml | 48 ++++++++++++++ pyproject.toml | 1 + uv.lock | 33 ++++++++++ 11 files changed, 280 insertions(+) create mode 120000 docs/source/reference/package-apis/drivers/vnc.md create mode 100644 packages/jumpstarter-driver-vnc/README.md create mode 100644 packages/jumpstarter-driver-vnc/examples/exporter.yaml create mode 100644 packages/jumpstarter-driver-vnc/jumpstarter_driver_vnc/__init__.py create mode 100644 packages/jumpstarter-driver-vnc/jumpstarter_driver_vnc/client.py create mode 100644 packages/jumpstarter-driver-vnc/jumpstarter_driver_vnc/driver.py create mode 100644 packages/jumpstarter-driver-vnc/jumpstarter_driver_vnc/driver_test.py create mode 100644 packages/jumpstarter-driver-vnc/pyproject.toml diff --git a/docs/source/reference/package-apis/drivers/index.md b/docs/source/reference/package-apis/drivers/index.md index 35bc68e72..c62e53faa 100644 --- a/docs/source/reference/package-apis/drivers/index.md +++ b/docs/source/reference/package-apis/drivers/index.md @@ -41,6 +41,7 @@ Drivers that provide various communication interfaces: Protocol * **[TFTP](tftp.md)** (`jumpstarter-driver-tftp`) - Trivial File Transfer Protocol +* **[VNC](vnc.md)** (`jumpstarter-driver-vnc`) - VNC (Virtual Network Computing) remote desktop protocol ### Storage and Data Drivers @@ -109,5 +110,6 @@ tmt.md tftp.md uboot.md ustreamer.md +vnc.md yepkit.md ``` diff --git a/docs/source/reference/package-apis/drivers/vnc.md b/docs/source/reference/package-apis/drivers/vnc.md new file mode 120000 index 000000000..e43158538 --- /dev/null +++ b/docs/source/reference/package-apis/drivers/vnc.md @@ -0,0 +1 @@ +../../../../../packages/jumpstarter-driver-vnc/README.md \ No newline at end of file diff --git a/packages/jumpstarter-driver-vnc/README.md b/packages/jumpstarter-driver-vnc/README.md new file mode 100644 index 000000000..248565a97 --- /dev/null +++ b/packages/jumpstarter-driver-vnc/README.md @@ -0,0 +1,58 @@ +# Vnc Driver + +`jumpstarter-driver-vnc` provides functionality for interacting with VNC servers. It allows you to create a secure, tunneled VNC session in your browser. + +## Installation + +```shell +pip3 install --extra-index-url https://pkg.jumpstarter.dev/simple/ jumpstarter-driver-vnc +``` + +## Configuration + +The VNC driver is a composite driver that requires a TCP child driver to establish the underlying network connection. The TCP driver should be configured to point to the VNC server's host and port, which is often `127.0.0.1` from the perspective of the Jumpstarter server. + +Example `exporter.yaml` configuration: + +```yaml +export: + vnc: + type: jumpstarter_driver_vnc.driver.Vnc + children: + tcp: + type: jumpstarter_driver_network.driver.TcpNetwork + config: + host: "127.0.0.1" + port: 5901 # Default VNC port for display :1 +``` + +## API Reference + +The client class for this driver is `jumpstarter_driver_vnc.client.VNClient`. + +### `vnc.session()` + +This asynchronous context manager establishes a connection to the remote VNC server and provides a local web server to view the session. + +**Usage:** + +```python +async with vnc.session() as novnc_adapter: + print(f"VNC session available at: {novnc_adapter.url}") + # The session remains open until the context block is exited. + await novnc_adapter.wait() +``` + +### CLI: `j vnc session` + +This driver provides a convenient CLI command within the `jmp shell`. By default, it will open the session URL in your default web browser. + +**Usage:** + +```shell +# This will start the local server and open a browser. +j vnc session + +# To prevent it from opening a browser automatically: +j vnc session --no-browser +``` diff --git a/packages/jumpstarter-driver-vnc/examples/exporter.yaml b/packages/jumpstarter-driver-vnc/examples/exporter.yaml new file mode 100644 index 000000000..2bf06d3a2 --- /dev/null +++ b/packages/jumpstarter-driver-vnc/examples/exporter.yaml @@ -0,0 +1,16 @@ +apiVersion: jumpstarter.dev/v1alpha1 +kind: ExporterConfig +metadata: + namespace: default + name: demo +endpoint: grpc.jumpstarter.192.168.0.203.nip.io:8082 +token: "" +export: + vnc: + type: jumpstarter_driver_vnc.driver.Vnc + children: + tcp: + type: jumpstarter_driver_network.driver.TcpNetwork + config: + host: "127.0.0.1" + port: 5901 # Default VNC port for display :1 diff --git a/packages/jumpstarter-driver-vnc/jumpstarter_driver_vnc/__init__.py b/packages/jumpstarter-driver-vnc/jumpstarter_driver_vnc/__init__.py new file mode 100644 index 000000000..1c735dd4d --- /dev/null +++ b/packages/jumpstarter-driver-vnc/jumpstarter_driver_vnc/__init__.py @@ -0,0 +1,3 @@ +from .client import VNClient + +VNClient = VNClient diff --git a/packages/jumpstarter-driver-vnc/jumpstarter_driver_vnc/client.py b/packages/jumpstarter-driver-vnc/jumpstarter_driver_vnc/client.py new file mode 100644 index 000000000..05c21549f --- /dev/null +++ b/packages/jumpstarter-driver-vnc/jumpstarter_driver_vnc/client.py @@ -0,0 +1,65 @@ +from __future__ import annotations + +import contextlib +import typing +import webbrowser + +import anyio +import click +from jumpstarter_driver_composite.client import CompositeClient +from jumpstarter_driver_network.adapters.novnc import NovncAdapter + +from jumpstarter.client.decorators import driver_click_group + +if typing.TYPE_CHECKING: + from jumpstarter_driver_network.client import TCPClient + + +class VNClient(CompositeClient): + """Client for interacting with a VNC server.""" + + @property + def tcp(self) -> TCPClient: + """Get the TCP client.""" + return typing.cast("TCPClient", self.children["tcp"]) + + def stream(self, method="connect"): + """Create a new stream, proxied to the underlying TCP driver.""" + return self.tcp.stream(method) + + async def stream_async(self, method="connect"): + """Create a new async stream, proxied to the underlying TCP driver.""" + return await self.tcp.stream_async(method) + + @contextlib.contextmanager + def session(self) -> typing.Iterator[str]: + """Create a new VNC session.""" + with NovncAdapter(client=self.tcp, method="connect") as adapter: + yield adapter + + def cli(self) -> click.Command: + """Return a click command handler for this driver.""" + + @driver_click_group(self) + def vnc(): + """Open a VNC session.""" + + @vnc.command() + @click.option("--browser/--no-browser", default=True, help="Open the session in a web browser.") + def session(browser: bool): + """Open a VNC session.""" + # The NovncAdapter is a blocking context manager that runs in a thread. + # We can enter it, open the browser, and then just wait for the user + # to press Ctrl+C to exit. The adapter handles the background work. + with self.session() as url: + click.echo(f"To connect, please visit: {url}") + if browser: + webbrowser.open(url) + click.echo("Press Ctrl+C to close the VNC session.") + try: + # Use the client's own portal to wait for cancellation. + self.portal.call(anyio.sleep_forever) + except (KeyboardInterrupt, anyio.get_cancelled_exc_class()): + click.echo("\nClosing VNC session.") + + return vnc diff --git a/packages/jumpstarter-driver-vnc/jumpstarter_driver_vnc/driver.py b/packages/jumpstarter-driver-vnc/jumpstarter_driver_vnc/driver.py new file mode 100644 index 000000000..68ea4fae8 --- /dev/null +++ b/packages/jumpstarter-driver-vnc/jumpstarter_driver_vnc/driver.py @@ -0,0 +1,19 @@ +from __future__ import annotations + +from jumpstarter.common.exceptions import ConfigurationError +from jumpstarter.driver import Driver + + +class Vnc(Driver): + """A driver for VNC.""" + + def __post_init__(self): + """Initialize the VNC driver.""" + super().__post_init__() + if "tcp" not in self.children: + raise ConfigurationError("A tcp child is required for Vnc") + + @classmethod + def client(cls) -> str: + """Return the client class path for this driver.""" + return "jumpstarter_driver_vnc.client.VNClient" diff --git a/packages/jumpstarter-driver-vnc/jumpstarter_driver_vnc/driver_test.py b/packages/jumpstarter-driver-vnc/jumpstarter_driver_vnc/driver_test.py new file mode 100644 index 000000000..7dba81eb3 --- /dev/null +++ b/packages/jumpstarter-driver-vnc/jumpstarter_driver_vnc/driver_test.py @@ -0,0 +1,34 @@ +from __future__ import annotations + +import pytest +from jumpstarter_driver_composite.client import CompositeClient + +from jumpstarter_driver_vnc.driver import Vnc + +from jumpstarter.client import DriverClient +from jumpstarter.common.exceptions import ConfigurationError +from jumpstarter.common.utils import serve +from jumpstarter.driver import Driver + + +class FakeTcpDriver(Driver): + @classmethod + def client(cls) -> str: + return "jumpstarter.client.DriverClient" + + +def test_vnc_client_is_composite(): + """Test that the Vnc driver produces a composite client.""" + instance = Vnc( + children={"tcp": FakeTcpDriver()}, + ) + + with serve(instance) as client: + assert isinstance(client, CompositeClient) + assert isinstance(client.tcp, DriverClient) + + +def test_vnc_driver_raises_error_without_tcp_child(): + """Test that the Vnc driver raises a ConfigurationError if the tcp child is missing.""" + with pytest.raises(ConfigurationError, match="A tcp child is required for Vnc"): + Vnc(children={}) diff --git a/packages/jumpstarter-driver-vnc/pyproject.toml b/packages/jumpstarter-driver-vnc/pyproject.toml new file mode 100644 index 000000000..49ce3545f --- /dev/null +++ b/packages/jumpstarter-driver-vnc/pyproject.toml @@ -0,0 +1,48 @@ +[project] +name = "jumpstarter-driver-vnc" +dynamic = ["version", "urls"] +description = "Jumpstarter driver for VNC" +readme = "README.md" +license = "Apache-2.0" +authors = [ + { name = "Albert Esteve", email = "aesteve@redhat.com" } +] +requires-python = ">=3.11" +dependencies = [ + "anyio>=4.10.0", + "jumpstarter", + "jumpstarter-driver-composite", + "jumpstarter-driver-network", + "click", +] + +[project.entry-points."jumpstarter.drivers"] +vnc = "jumpstarter_driver_vnc.driver:Vnc" + +[tool.hatch.version] +source = "vcs" +raw-options = { 'root' = '../../'} + +[tool.hatch.metadata.hooks.vcs.urls] +Homepage = "https://jumpstarter.dev" +source_archive = "https://github.com/jumpstarter-dev/jumpstarter/archive/{commit_hash}.zip" + +[tool.pytest.ini_options] +addopts = "--cov --cov-report=html --cov-report=xml" +log_cli = true +log_cli_level = "INFO" +testpaths = ["jumpstarter_driver_vnc"] +asyncio_mode = "auto" + +[build-system] +requires = ["hatchling", "hatch-vcs", "hatch-pin-jumpstarter"] +build-backend = "hatchling.build" + +[tool.hatch.build.hooks.pin_jumpstarter] +name = "pin_jumpstarter" + +[dependency-groups] +dev = [ + "pytest-cov>=6.0.0", + "pytest>=8.3.3", +] diff --git a/pyproject.toml b/pyproject.toml index e99fbdbf1..3338588be 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -34,6 +34,7 @@ jumpstarter-driver-uboot = { workspace = true } jumpstarter-driver-iscsi = { workspace = true } jumpstarter-driver-ustreamer = { workspace = true } jumpstarter-driver-yepkit = { workspace = true } +jumpstarter-driver-vnc = { workspace = true } jumpstarter-imagehash = { workspace = true } jumpstarter-kubernetes = { workspace = true } jumpstarter-protocol = { workspace = true } diff --git a/uv.lock b/uv.lock index 49fb171e4..ab29cefb8 100644 --- a/uv.lock +++ b/uv.lock @@ -37,6 +37,7 @@ members = [ "jumpstarter-driver-tmt", "jumpstarter-driver-uboot", "jumpstarter-driver-ustreamer", + "jumpstarter-driver-vnc", "jumpstarter-driver-yepkit", "jumpstarter-example-automotive", "jumpstarter-example-soc-pytest", @@ -2127,6 +2128,38 @@ dev = [ { name = "pytest-cov", specifier = ">=5.0.0" }, ] +[[package]] +name = "jumpstarter-driver-vnc" +source = { editable = "packages/jumpstarter-driver-vnc" } +dependencies = [ + { name = "anyio" }, + { name = "click" }, + { name = "jumpstarter" }, + { name = "jumpstarter-driver-composite" }, + { name = "jumpstarter-driver-network" }, +] + +[package.dev-dependencies] +dev = [ + { name = "pytest" }, + { name = "pytest-cov" }, +] + +[package.metadata] +requires-dist = [ + { name = "anyio", specifier = ">=4.10.0" }, + { name = "click" }, + { name = "jumpstarter", editable = "packages/jumpstarter" }, + { name = "jumpstarter-driver-composite", editable = "packages/jumpstarter-driver-composite" }, + { name = "jumpstarter-driver-network", editable = "packages/jumpstarter-driver-network" }, +] + +[package.metadata.requires-dev] +dev = [ + { name = "pytest", specifier = ">=8.3.3" }, + { name = "pytest-cov", specifier = ">=6.0.0" }, +] + [[package]] name = "jumpstarter-driver-yepkit" source = { editable = "packages/jumpstarter-driver-yepkit" } From 7fd86267d89f920000816fe41728284a36c0003c Mon Sep 17 00:00:00 2001 From: Albert Esteve Date: Thu, 11 Dec 2025 17:38:19 +0100 Subject: [PATCH 2/3] novnc: add encrypt parameter Add encrypt parameter to novnc adapter class, so that it can be configured in the url params. This allows the VNC driver to set --encrypt/--no-encrypt parameter through command line (default False) for more flexibility on the type of connection the user needs. Signed-off-by: Albert Esteve (cherry picked from commit ff46bade1cd3143fdef4a9348ff355bab3a2d859) --- .../adapters/novnc.py | 12 +++++++-- packages/jumpstarter-driver-vnc/README.md | 10 +++++++ .../examples/exporter.yaml | 3 +++ .../jumpstarter_driver_vnc/client.py | 26 ++++++++++++++++--- .../jumpstarter_driver_vnc/driver.py | 22 +++++++++++++--- .../jumpstarter_driver_vnc/driver_test.py | 8 ++++++ 6 files changed, 72 insertions(+), 9 deletions(-) diff --git a/packages/jumpstarter-driver-network/jumpstarter_driver_network/adapters/novnc.py b/packages/jumpstarter-driver-network/jumpstarter_driver_network/adapters/novnc.py index 1a64c478b..a95cda170 100644 --- a/packages/jumpstarter-driver-network/jumpstarter_driver_network/adapters/novnc.py +++ b/packages/jumpstarter-driver-network/jumpstarter_driver_network/adapters/novnc.py @@ -10,7 +10,7 @@ @blocking @asynccontextmanager -async def NovncAdapter(*, client: DriverClient, method: str = "connect"): +async def NovncAdapter(*, client: DriverClient, method: str = "connect", encrypt: bool = True): async def handler(conn): async with conn: async with client.stream_async(method) as stream: @@ -19,13 +19,21 @@ async def handler(conn): pass async with TemporaryTcpListener(handler) as addr: + params = { + "encrypt": 1 if encrypt else 0, + "autoconnect": 1, + "reconnect": 1, + "host": addr[0], + "port": addr[1], + } + yield urlunparse( ( "https", "novnc.com", "/noVNC/vnc.html", "", - urlencode({"autoconnect": 1, "reconnect": 1, "host": addr[0], "port": addr[1]}), + urlencode(params), "", ) ) diff --git a/packages/jumpstarter-driver-vnc/README.md b/packages/jumpstarter-driver-vnc/README.md index 248565a97..58f8baebc 100644 --- a/packages/jumpstarter-driver-vnc/README.md +++ b/packages/jumpstarter-driver-vnc/README.md @@ -18,6 +18,9 @@ Example `exporter.yaml` configuration: export: vnc: type: jumpstarter_driver_vnc.driver.Vnc + # You can set the default encryption behavior for the `j vnc session` command. + # If not set, it defaults to False (unencrypted). + default_encrypt: false children: tcp: type: jumpstarter_driver_network.driver.TcpNetwork @@ -55,4 +58,11 @@ j vnc session # To prevent it from opening a browser automatically: j vnc session --no-browser + +# To force an encrypted (wss://) or unencrypted (ws://) connection, overriding +# the default set in the exporter configuration: +j vnc session --encrypt +j vnc session --no-encrypt ``` + +> **Note:** Using an encrypted connection is intended for advanced scenarios where the local proxy can be configured with a TLS certificate that your browser trusts. For standard local development, modern browsers will likely reject the self-signed certificate and the connection will fail. diff --git a/packages/jumpstarter-driver-vnc/examples/exporter.yaml b/packages/jumpstarter-driver-vnc/examples/exporter.yaml index 2bf06d3a2..77e3e7f65 100644 --- a/packages/jumpstarter-driver-vnc/examples/exporter.yaml +++ b/packages/jumpstarter-driver-vnc/examples/exporter.yaml @@ -8,6 +8,9 @@ token: "" export: vnc: type: jumpstarter_driver_vnc.driver.Vnc + # You can set the default encryption behavior for the `j vnc session` command. + # If not set, it defaults to False (unencrypted). + default_encrypt: false children: tcp: type: jumpstarter_driver_network.driver.TcpNetwork diff --git a/packages/jumpstarter-driver-vnc/jumpstarter_driver_vnc/client.py b/packages/jumpstarter-driver-vnc/jumpstarter_driver_vnc/client.py index 05c21549f..2c6dec30b 100644 --- a/packages/jumpstarter-driver-vnc/jumpstarter_driver_vnc/client.py +++ b/packages/jumpstarter-driver-vnc/jumpstarter_driver_vnc/client.py @@ -32,11 +32,15 @@ async def stream_async(self, method="connect"): return await self.tcp.stream_async(method) @contextlib.contextmanager - def session(self) -> typing.Iterator[str]: + def session(self, *, encrypt: bool = True) -> typing.Iterator[str]: """Create a new VNC session.""" - with NovncAdapter(client=self.tcp, method="connect") as adapter: + with NovncAdapter(client=self.tcp, method="connect", encrypt=encrypt) as adapter: yield adapter + def get_default_encrypt(self) -> bool: + """Fetch the default encryption setting from the remote driver.""" + return typing.cast(bool, self.call("get_default_encrypt")) + def cli(self) -> click.Command: """Return a click command handler for this driver.""" @@ -46,12 +50,26 @@ def vnc(): @vnc.command() @click.option("--browser/--no-browser", default=True, help="Open the session in a web browser.") - def session(browser: bool): + @click.option( + "--encrypt", + "encrypt_override", + flag_value=True, + default=None, + help="Force an encrypted connection (wss://). Overrides the driver default.", + ) + @click.option( + "--no-encrypt", + "encrypt_override", + flag_value=False, + help="Force an unencrypted connection (ws://). Overrides the driver default.", + ) + def session(browser: bool, encrypt_override: bool | None): """Open a VNC session.""" + encrypt = encrypt_override if encrypt_override is not None else self.get_default_encrypt() # The NovncAdapter is a blocking context manager that runs in a thread. # We can enter it, open the browser, and then just wait for the user # to press Ctrl+C to exit. The adapter handles the background work. - with self.session() as url: + with self.session(encrypt=encrypt) as url: click.echo(f"To connect, please visit: {url}") if browser: webbrowser.open(url) diff --git a/packages/jumpstarter-driver-vnc/jumpstarter_driver_vnc/driver.py b/packages/jumpstarter-driver-vnc/jumpstarter_driver_vnc/driver.py index 68ea4fae8..483cd2ae7 100644 --- a/packages/jumpstarter-driver-vnc/jumpstarter_driver_vnc/driver.py +++ b/packages/jumpstarter-driver-vnc/jumpstarter_driver_vnc/driver.py @@ -1,11 +1,22 @@ from __future__ import annotations +from dataclasses import dataclass + +from jumpstarter_driver_composite.driver import Composite + from jumpstarter.common.exceptions import ConfigurationError -from jumpstarter.driver import Driver +from jumpstarter.driver import export + +@dataclass +class Vnc(Composite): + """A VNC driver. -class Vnc(Driver): - """A driver for VNC.""" + Members: + default_encrypt: Whether to default to an encrypted client connection. + """ + + default_encrypt: bool = False def __post_init__(self): """Initialize the VNC driver.""" @@ -13,6 +24,11 @@ def __post_init__(self): if "tcp" not in self.children: raise ConfigurationError("A tcp child is required for Vnc") + @export + async def get_default_encrypt(self) -> bool: + """Return the default encryption setting.""" + return self.default_encrypt + @classmethod def client(cls) -> str: """Return the client class path for this driver.""" diff --git a/packages/jumpstarter-driver-vnc/jumpstarter_driver_vnc/driver_test.py b/packages/jumpstarter-driver-vnc/jumpstarter_driver_vnc/driver_test.py index 7dba81eb3..5abea3337 100644 --- a/packages/jumpstarter-driver-vnc/jumpstarter_driver_vnc/driver_test.py +++ b/packages/jumpstarter-driver-vnc/jumpstarter_driver_vnc/driver_test.py @@ -32,3 +32,11 @@ def test_vnc_driver_raises_error_without_tcp_child(): """Test that the Vnc driver raises a ConfigurationError if the tcp child is missing.""" with pytest.raises(ConfigurationError, match="A tcp child is required for Vnc"): Vnc(children={}) + + +@pytest.mark.parametrize("expected", [True, False]) +def test_vnc_driver_default_encrypt(expected): + """Test that the default_encrypt parameter is correctly handled.""" + instance = Vnc(children={"tcp": FakeTcpDriver()}, default_encrypt=expected) + with serve(instance) as client: + assert client.get_default_encrypt() is expected From 0a0abceffe5229fdfa261220f2eab15dcd3da103 Mon Sep 17 00:00:00 2001 From: Albert Esteve Date: Fri, 12 Dec 2025 09:38:02 +0100 Subject: [PATCH 3/3] novnc: Improve docstrings for coverage Signed-off-by: Albert Esteve (cherry picked from commit 6cdcf5c258ce82568222441011fbb8192a44f988) --- .../adapters/novnc.py | 15 +++++ .../jumpstarter_driver_vnc/client.py | 56 ++++++++++++++++--- .../jumpstarter_driver_vnc/driver.py | 15 ++++- 3 files changed, 77 insertions(+), 9 deletions(-) diff --git a/packages/jumpstarter-driver-network/jumpstarter_driver_network/adapters/novnc.py b/packages/jumpstarter-driver-network/jumpstarter_driver_network/adapters/novnc.py index a95cda170..f6eb6058e 100644 --- a/packages/jumpstarter-driver-network/jumpstarter_driver_network/adapters/novnc.py +++ b/packages/jumpstarter-driver-network/jumpstarter_driver_network/adapters/novnc.py @@ -11,6 +11,21 @@ @blocking @asynccontextmanager async def NovncAdapter(*, client: DriverClient, method: str = "connect", encrypt: bool = True): + """ + Provide a noVNC URL that proxies a temporary local TCP listener to a remote + driver stream via a WebSocket bridge. + + Parameters: + client (DriverClient): Client used to open the remote stream that will be + bridged to the local listener. + method (str): Name of the async stream method to call on the client (default "connect"). + encrypt (bool): If True request an encrypted (TLS) vnc connection; + if False request an unencrypted vnc connection. + + Returns: + str: The URL to connect to the VNC session. + """ + async def handler(conn): async with conn: async with client.stream_async(method) as stream: diff --git a/packages/jumpstarter-driver-vnc/jumpstarter_driver_vnc/client.py b/packages/jumpstarter-driver-vnc/jumpstarter_driver_vnc/client.py index 2c6dec30b..b2ff4d004 100644 --- a/packages/jumpstarter-driver-vnc/jumpstarter_driver_vnc/client.py +++ b/packages/jumpstarter-driver-vnc/jumpstarter_driver_vnc/client.py @@ -20,7 +20,12 @@ class VNClient(CompositeClient): @property def tcp(self) -> TCPClient: - """Get the TCP client.""" + """ + Access the underlying TCP client. + + Returns: + TCPClient: The TCP client instance stored in this composite client's children mapping. + """ return typing.cast("TCPClient", self.children["tcp"]) def stream(self, method="connect"): @@ -33,7 +38,15 @@ async def stream_async(self, method="connect"): @contextlib.contextmanager def session(self, *, encrypt: bool = True) -> typing.Iterator[str]: - """Create a new VNC session.""" + """ + Open a noVNC session and yield the connection URL. + + Parameters: + encrypt (bool): If True, request an encrypted vnc connection. + + Returns: + url (str): The URL to connect to the VNC session. + """ with NovncAdapter(client=self.tcp, method="connect", encrypt=encrypt) as adapter: yield adapter @@ -42,11 +55,28 @@ def get_default_encrypt(self) -> bool: return typing.cast(bool, self.call("get_default_encrypt")) def cli(self) -> click.Command: - """Return a click command handler for this driver.""" + """ + Provide a Click command group for running VNC sessions. + + The returned command exposes a `session` subcommand that opens a VNC session, + prints the connection URL, optionally opens it in the user's browser, + and waits until the user cancels the session. + + Returns: + click.Command: Click command group with a `session` subcommand that accepts + `--browser/--no-browser` and `--encrypt/--no-encrypt` options. + """ @driver_click_group(self) def vnc(): - """Open a VNC session.""" + """ + Open a VNC session and block until the user closes it. + + When invoked, prints the connection URL for the noVNC session, optionally + opens that URL in the user's web browser, and waits for user-initiated + termination (for example, Ctrl+C). On exit, prints a message indicating + the session is closing. + """ @vnc.command() @click.option("--browser/--no-browser", default=True, help="Open the session in a web browser.") @@ -55,16 +85,28 @@ def vnc(): "encrypt_override", flag_value=True, default=None, - help="Force an encrypted connection (wss://). Overrides the driver default.", + help="Force an encrypted vnc connection. Overrides the driver default.", ) @click.option( "--no-encrypt", "encrypt_override", flag_value=False, - help="Force an unencrypted connection (ws://). Overrides the driver default.", + help="Force an unencrypted vnc connection. Overrides the driver default.", ) def session(browser: bool, encrypt_override: bool | None): - """Open a VNC session.""" + """ + Open an interactive VNC session and wait for the user to terminate it. + + Starts a VNC session using the client's session context, prints the connection + URL, optionally opens that URL in a web browser, and blocks until the user + cancels (e.g., Ctrl+C), then closes the session. + + Parameters: + browser (bool): If True, open the session URL in the default web browser. + encrypt_override (bool | None): If provided, overrides the driver's default + encryption setting. True for encrypted, + False for unencrypted, None to use driver default. + """ encrypt = encrypt_override if encrypt_override is not None else self.get_default_encrypt() # The NovncAdapter is a blocking context manager that runs in a thread. # We can enter it, open the browser, and then just wait for the user diff --git a/packages/jumpstarter-driver-vnc/jumpstarter_driver_vnc/driver.py b/packages/jumpstarter-driver-vnc/jumpstarter_driver_vnc/driver.py index 483cd2ae7..29cb7ced0 100644 --- a/packages/jumpstarter-driver-vnc/jumpstarter_driver_vnc/driver.py +++ b/packages/jumpstarter-driver-vnc/jumpstarter_driver_vnc/driver.py @@ -19,7 +19,13 @@ class Vnc(Composite): default_encrypt: bool = False def __post_init__(self): - """Initialize the VNC driver.""" + """ + Validate the VNC driver's post-initialization configuration. + Ensures the driver has a "tcp" child configured. + + Raises: + ConfigurationError: If a "tcp" child is not present. + """ super().__post_init__() if "tcp" not in self.children: raise ConfigurationError("A tcp child is required for Vnc") @@ -31,5 +37,10 @@ async def get_default_encrypt(self) -> bool: @classmethod def client(cls) -> str: - """Return the client class path for this driver.""" + """ + Client class path for this driver. + + Returns: + str: Dotted import path of the client class. + """ return "jumpstarter_driver_vnc.client.VNClient"