Skip to content
This repository was archived by the owner on Jan 23, 2026. It is now read-only.

Commit fd5c70e

Browse files
committed
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 <aesteve@redhat.com>
1 parent fa0a3b5 commit fd5c70e

File tree

11 files changed

+273
-0
lines changed

11 files changed

+273
-0
lines changed

docs/source/reference/package-apis/drivers/index.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ Drivers that provide various communication interfaces:
4242
Protocol
4343
* **[TFTP](tftp.md)** (`jumpstarter-driver-tftp`) - Trivial File Transfer
4444
Protocol
45+
* **[VNC](vnc.md)** (`jumpstarter-driver-vnc`) - VNC (Virtual Network Computing) remote desktop protocol
4546

4647
### Storage and Data Drivers
4748

@@ -111,5 +112,6 @@ tmt.md
111112
tftp.md
112113
uboot.md
113114
ustreamer.md
115+
vnc.md
114116
yepkit.md
115117
```
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
../../../../../packages/jumpstarter-driver-vnc/README.md
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
# Vnc Driver
2+
3+
`jumpstarter-driver-vnc` provides functionality for interacting with VNC servers. It allows you to create a secure, tunneled VNC session in your browser.
4+
5+
## Installation
6+
7+
```shell
8+
pip3 install --extra-index-url https://pkg.jumpstarter.dev/simple/ jumpstarter-driver-vnc
9+
```
10+
11+
## Configuration
12+
13+
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.
14+
15+
Example `exporter.yaml` configuration:
16+
17+
```yaml
18+
export:
19+
vnc:
20+
type: jumpstarter_driver_vnc.driver.Vnc
21+
children:
22+
tcp:
23+
type: jumpstarter_driver_network.driver.TcpNetwork
24+
config:
25+
host: "127.0.0.1"
26+
port: 5901 # Default VNC port for display :1
27+
```
28+
29+
## API Reference
30+
31+
The client class for this driver is `jumpstarter_driver_vnc.client.VNClient`.
32+
33+
### `vnc.session()`
34+
35+
This asynchronous context manager establishes a connection to the remote VNC server and provides a local web server to view the session.
36+
37+
**Usage:**
38+
39+
```python
40+
async with vnc.session() as novnc_adapter:
41+
print(f"VNC session available at: {novnc_adapter.url}")
42+
# The session remains open until the context block is exited.
43+
await novnc_adapter.wait()
44+
```
45+
46+
### CLI: `j vnc session`
47+
48+
This driver provides a convenient CLI command within the `jmp shell`. By default, it will open the session URL in your default web browser.
49+
50+
**Usage:**
51+
52+
```shell
53+
# This will start the local server and open a browser.
54+
j vnc session
55+
56+
# To prevent it from opening a browser automatically:
57+
j vnc session --no-browser
58+
```
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
apiVersion: jumpstarter.dev/v1alpha1
2+
kind: ExporterConfig
3+
metadata:
4+
namespace: default
5+
name: demo
6+
endpoint: grpc.jumpstarter.192.168.0.203.nip.io:8082
7+
token: "<token>"
8+
export:
9+
vnc:
10+
type: jumpstarter_driver_vnc.driver.Vnc
11+
children:
12+
tcp:
13+
type: jumpstarter_driver_network.driver.TcpNetwork
14+
config:
15+
host: "127.0.0.1"
16+
port: 5901 # Default VNC port for display :1
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
from .client import VNClient
2+
3+
VNClient = VNClient
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
from __future__ import annotations
2+
3+
import asyncio
4+
import contextlib
5+
import typing
6+
import webbrowser
7+
8+
import anyio
9+
import click
10+
from jumpstarter_driver_composite.client import CompositeClient
11+
from jumpstarter_driver_network.adapters.novnc import NovncAdapter
12+
13+
from jumpstarter.client.decorators import driver_click_group
14+
15+
if typing.TYPE_CHECKING:
16+
from jumpstarter_driver_network.client import TCPClient
17+
18+
19+
class VNClient(CompositeClient):
20+
"""Client for interacting with a VNC server."""
21+
22+
@property
23+
def tcp(self) -> TCPClient:
24+
"""Get the TCP client."""
25+
return self.children["tcp"]
26+
27+
@contextlib.contextmanager
28+
def session(self) -> typing.Iterator[str]:
29+
"""Create a new VNC session."""
30+
with NovncAdapter(client=self.tcp, method="connect") as adapter:
31+
yield adapter
32+
33+
def cli(self) -> click.Command:
34+
"""Return a click command handler for this driver."""
35+
36+
@driver_click_group(self)
37+
def vnc():
38+
"""Open a VNC session."""
39+
40+
@vnc.command()
41+
@click.option("--browser/--no-browser", default=True, help="Open the session in a web browser.")
42+
def session(browser: bool):
43+
"""Open a VNC session."""
44+
# The NovncAdapter is a blocking context manager that runs in a thread.
45+
# We can enter it, open the browser, and then just wait for the user
46+
# to press Ctrl+C to exit. The adapter handles the background work.
47+
with self.session() as url:
48+
click.echo(f"To connect, please visit: {url}")
49+
if browser:
50+
webbrowser.open(url)
51+
click.echo("Press Ctrl+C to close the VNC session.")
52+
try:
53+
# Use the client's own portal to wait for cancellation.
54+
self.portal.call(asyncio.Event().wait)
55+
except (KeyboardInterrupt, anyio.get_cancelled_exc_class()):
56+
click.echo("\nClosing VNC session.")
57+
58+
return vnc
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
from __future__ import annotations
2+
3+
from jumpstarter.common.exceptions import ConfigurationError
4+
from jumpstarter.driver import Driver
5+
6+
7+
class Vnc(Driver):
8+
"""A driver for VNC."""
9+
10+
def __post_init__(self):
11+
"""Initialize the VNC driver."""
12+
super().__post_init__()
13+
if "tcp" not in self.children:
14+
raise ConfigurationError("A tcp child is required for Vnc")
15+
16+
@classmethod
17+
def client(cls) -> str:
18+
"""Return the client class path for this driver."""
19+
return "jumpstarter_driver_vnc.client.VNClient"
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
from __future__ import annotations
2+
3+
import pytest
4+
from jumpstarter_driver_composite.client import CompositeClient
5+
6+
from jumpstarter_driver_vnc.driver import Vnc
7+
8+
from jumpstarter.client import DriverClient
9+
from jumpstarter.common.exceptions import ConfigurationError
10+
from jumpstarter.common.utils import serve
11+
from jumpstarter.driver import Driver
12+
13+
14+
class FakeTcpDriver(Driver):
15+
@classmethod
16+
def client(cls) -> str:
17+
return "jumpstarter.client.DriverClient"
18+
19+
20+
def test_vnc_client_is_composite():
21+
"""Test that the Vnc driver produces a composite client."""
22+
instance = Vnc(
23+
children={"tcp": FakeTcpDriver()},
24+
)
25+
26+
with serve(instance) as client:
27+
assert isinstance(client, CompositeClient)
28+
assert isinstance(client.tcp, DriverClient)
29+
30+
31+
def test_vnc_driver_raises_error_without_tcp_child():
32+
"""Test that the Vnc driver raises a ConfigurationError if the tcp child is missing."""
33+
with pytest.raises(ConfigurationError, match="A tcp child is required for Vnc"):
34+
Vnc(children={})
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
[project]
2+
name = "jumpstarter-driver-vnc"
3+
dynamic = ["version", "urls"]
4+
description = "Jumpstarter driver for VNC"
5+
readme = "README.md"
6+
license = "Apache-2.0"
7+
authors = [
8+
{ name = "Albert Esteve", email = "aesteve@redhat.com" }
9+
]
10+
requires-python = ">=3.11"
11+
dependencies = [
12+
"anyio>=4.10.0",
13+
"jumpstarter",
14+
"jumpstarter-driver-composite",
15+
"jumpstarter-driver-network",
16+
"click",
17+
]
18+
19+
[project.entry-points."jumpstarter.drivers"]
20+
vnc = "jumpstarter_driver_vnc.driver:Vnc"
21+
22+
[tool.hatch.version]
23+
source = "vcs"
24+
raw-options = { 'root' = '../../'}
25+
26+
[tool.hatch.metadata.hooks.vcs.urls]
27+
Homepage = "https://jumpstarter.dev"
28+
source_archive = "https://github.com/jumpstarter-dev/jumpstarter-driver-vnc/archive/{commit_hash}.zip"
29+
30+
[tool.pytest.ini_options]
31+
addopts = "--cov --cov-report=html --cov-report=xml"
32+
log_cli = true
33+
log_cli_level = "INFO"
34+
testpaths = ["jumpstarter_driver_vnc"]
35+
asyncio_mode = "auto"
36+
37+
[build-system]
38+
requires = ["hatchling", "hatch-vcs", "hatch-pin-jumpstarter"]
39+
build-backend = "hatchling.build"
40+
41+
[tool.hatch.build.hooks.pin_jumpstarter]
42+
name = "pin_jumpstarter"
43+
44+
[dependency-groups]
45+
dev = [
46+
"pytest-cov>=6.0.0",
47+
"pytest>=8.3.3",
48+
]

pyproject.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ jumpstarter-driver-uboot = { workspace = true }
3535
jumpstarter-driver-iscsi = { workspace = true }
3636
jumpstarter-driver-ustreamer = { workspace = true }
3737
jumpstarter-driver-yepkit = { workspace = true }
38+
jumpstarter-driver-vnc = { workspace = true }
3839
jumpstarter-imagehash = { workspace = true }
3940
jumpstarter-kubernetes = { workspace = true }
4041
jumpstarter-protocol = { workspace = true }

0 commit comments

Comments
 (0)