Skip to content
This repository was archived by the owner on Jan 23, 2026. It is now read-only.
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
52 changes: 52 additions & 0 deletions docs/source/api-reference/drivers/dbus.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
# DbusNetwork driver

The DbusNetwork driver is a driver for transparently accessing the dbus on the remote machine.

Comment thread
NickCao marked this conversation as resolved.
## Driver configuration

```{literalinclude} dbus.yaml
:language: yaml
```

```{doctest}
:hide:
>>> from jumpstarter.config import ExporterConfigV1Alpha1DriverInstance
>>> ExporterConfigV1Alpha1DriverInstance.from_path("source/api-reference/drivers/dbus.yaml").instantiate()
DbusNetwork(...)
```

## Client API

```{eval-rst}
.. autoclass:: jumpstarter_driver_network.client.DbusNetworkClient()
:members:
```

Get machine id of the remote machine

```{doctest}
>>> with dbus:
... print(subprocess.run([
... "busctl",
... "call",
... "org.freedesktop.systemd1",
... "/org/freedesktop/systemd1",
... "org.freedesktop.DBus.Peer",
... "GetMachineId"
... ], stdout=subprocess.PIPE).stdout.decode()) # s "34df62c767c846d5a93eb2d6f05d9e1d"
s ...
```

```{testsetup} *
from jumpstarter_driver_network.driver import DbusNetwork
from jumpstarter.common.utils import serve
import subprocess

instance = serve(DbusNetwork(kind="session"))

dbus = instance.__enter__()
```

```{testcleanup} *
instance.__exit__(None, None, None)
```
3 changes: 3 additions & 0 deletions docs/source/api-reference/drivers/dbus.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
type: "jumpstarter_driver_network.driver.DbusNetwork"
config:
kind: "system" # which bus to connect to, system or session
1 change: 1 addition & 0 deletions docs/source/api-reference/drivers/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,4 +17,5 @@ snmp.md
tftp.md
ustreamer.md
yepkit.md
dbus.md
```
Original file line number Diff line number Diff line change
@@ -1,6 +1,14 @@
from .dbus import DbusAdapter
from .fabric import FabricAdapter
from .novnc import NovncAdapter
from .pexpect import PexpectAdapter
from .portforward import TcpPortforwardAdapter, UnixPortforwardAdapter

__all__ = ["FabricAdapter", "NovncAdapter", "PexpectAdapter", "TcpPortforwardAdapter", "UnixPortforwardAdapter"]
__all__ = [
"DbusAdapter",
"FabricAdapter",
"NovncAdapter",
"PexpectAdapter",
"TcpPortforwardAdapter",
"UnixPortforwardAdapter",
]
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
from dataclasses import dataclass
from os import environ, getenv

from .portforward import TcpPortforwardAdapter


@dataclass(kw_only=True)
class DbusAdapter(TcpPortforwardAdapter):
async def __aenter__(self):
addr = await super().__aenter__()
match self.client.kind:
case "system":
self.varname = "DBUS_SYSTEM_BUS_ADDRESS"
pass
case "session":
self.varname = "DBUS_SESSION_BUS_ADDRESS"
pass
case _:
raise ValueError(f"invalid bus type: {self.client.kind}")
self.oldenv = getenv(self.varname)
environ[self.varname] = f"tcp:host={addr[0]},port={addr[1]}"

async def __aexit__(self, exc_type, exc_value, traceback):
await super().__aexit__(exc_type, exc_value, traceback)
if self.oldenv is None:
del environ[self.varname]
else:
environ[self.varname] = self.oldenv
Original file line number Diff line number Diff line change
@@ -1,5 +1,22 @@
from contextlib import AbstractContextManager

from .adapters import DbusAdapter
from .driver import DbusNetwork
from jumpstarter.client import DriverClient


class NetworkClient(DriverClient):
pass


class DbusNetworkClient(NetworkClient, AbstractContextManager):
def __enter__(self):
self.adapter = DbusAdapter(client=self)
self.adapter.__enter__()

def __exit__(self, exc_type, exc_value, traceback):
self.adapter.__exit__(exc_type, exc_value, traceback)

@property
def kind(self):
return self.labels[DbusNetwork.KIND_LABEL]
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
from abc import ABCMeta, abstractmethod
from contextlib import asynccontextmanager
from dataclasses import dataclass
from dataclasses import dataclass, field
from os import getenv, getuid
from typing import ClassVar, Literal

from anyio import (
connect_tcp,
Expand Down Expand Up @@ -102,6 +104,76 @@ async def connect(self):
yield stream


@dataclass(kw_only=True)
class DbusNetwork(NetworkInterface, Driver):
kind: Literal["system", "session"]

scheme: str | None = field(init=False, default=None)
args: dict[str, str] = field(init=False, default_factory=dict)

KIND_LABEL: ClassVar[str] = "jumpstarter.dev/dbusnetwork/kind"

@classmethod
def client(cls) -> str:
return "jumpstarter_driver_network.client.DbusNetworkClient"

def extra_labels(self):
return {self.KIND_LABEL: self.kind}

def __post_init__(self): # noqa: C901
if hasattr(super(), "__post_init__"):
super().__post_init__()

match self.kind:
case "system":
bus = getenv("DBUS_SYSTEM_BUS_ADDRESS", "unix:path=/run/dbus/system_bus_socket")
case "session":
bus = getenv("DBUS_SESSION_BUS_ADDRESS", f"unix:path=/run/user/{getuid()}/bus")
case _:
raise ValueError(f"invalid bus type: {self.kind}")

self.scheme, sep, rem = bus.partition(":")
if not sep:
raise ValueError(f"invalid bus addr: {bus}")

for part in rem.split(","):
key, sep, value = part.partition("=")
if not sep:
raise ValueError(f"invalid bus addr: {bus}, missing separator in arguments")
self.args[key] = value

match self.scheme:
case "unix":
if "path" not in self.args:
raise ValueError(f"invalid bus addr: {bus}, missing path argument")
case "tcp":
if "host" not in self.args:
raise ValueError(f"invalid bus addr: {bus}, missing host argument")
if "port" not in self.args:
raise ValueError(f"invalid bus addr: {bus}, missing port argument")

try:
port = int(self.args["port"])
except ValueError as e:
raise ValueError(f"invalid bus addr: {bus}, invalid port argument") from e
self.args["port"] = port
case _:
raise ValueError(f"invalid bus scheme: {self.scheme}")

@exportstream
@asynccontextmanager
async def connect(self):
match self.scheme:
case "unix":
self.logger.debug("Connecting UDS path=%s", self.args["path"])
async with await connect_unix(path=self.args["path"]) as stream:
yield stream
case "tcp":
self.logger.debug("Connecting TCP host=%s port=%d", self.args["host"], self.args["port"])
async with await connect_tcp(remote_host=self.args["host"], remote_port=self.args["port"]) as stream:
yield stream

Comment on lines +163 to +175
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Add error handling for connection failures.

The connection logic should handle potential network errors and provide meaningful error messages.

 @exportstream
 @asynccontextmanager
 async def connect(self):
+    try:
         match self.scheme:
             case "unix":
                 self.logger.debug("Connecting UDS path=%s", self.args["path"])
                 async with await connect_unix(path=self.args["path"]) as stream:
                     yield stream
             case "tcp":
                 self.logger.debug("Connecting TCP host=%s port=%d", self.args["host"], self.args["port"])
                 async with await connect_tcp(remote_host=self.args["host"], remote_port=self.args["port"]) as stream:
                     yield stream
+    except ConnectionError as e:
+        self.logger.error("Failed to connect to %s DBus: %s", self.kind, e)
+        raise ValueError(f"Failed to connect to {self.kind} DBus: {e}") from e
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
@exportstream
@asynccontextmanager
async def connect(self):
match self.scheme:
case "unix":
self.logger.debug("Connecting UDS path=%s", self.args["path"])
async with await connect_unix(path=self.args["path"]) as stream:
yield stream
case "tcp":
self.logger.debug("Connecting TCP host=%s port=%d", self.args["host"], self.args["port"])
async with await connect_tcp(remote_host=self.args["host"], remote_port=self.args["port"]) as stream:
yield stream
@exportstream
@asynccontextmanager
async def connect(self):
try:
match self.scheme:
case "unix":
self.logger.debug("Connecting UDS path=%s", self.args["path"])
async with await connect_unix(path=self.args["path"]) as stream:
yield stream
case "tcp":
self.logger.debug("Connecting TCP host=%s port=%d", self.args["host"], self.args["port"])
async with await connect_tcp(remote_host=self.args["host"], remote_port=self.args["port"]) as stream:
yield stream
except ConnectionError as e:
self.logger.error("Failed to connect to %s DBus: %s", self.kind, e)
raise ValueError(f"Failed to connect to {self.kind} DBus: {e}") from e

🛠️ Refactor suggestion

Consider adding error handling for connection failures.

The connect method should handle potential connection failures and provide meaningful error messages.

     @exportstream
     @asynccontextmanager
     async def connect(self):
+        try:
             match self.scheme:
                 case "unix":
                     self.logger.debug("Connecting UDS path=%s", self.args["path"])
                     async with await connect_unix(path=self.args["path"]) as stream:
                         yield stream
                 case "tcp":
                     self.logger.debug("Connecting TCP host=%s port=%d", self.args["host"], self.args["port"])
                     async with await connect_tcp(remote_host=self.args["host"], remote_port=self.args["port"]) as stream:
                         yield stream
+        except OSError as e:
+            raise ConnectionError(f"Failed to connect to {self.kind} bus: {e}") from e
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
@exportstream
@asynccontextmanager
async def connect(self):
match self.scheme:
case "unix":
self.logger.debug("Connecting UDS path=%s", self.args["path"])
async with await connect_unix(path=self.args["path"]) as stream:
yield stream
case "tcp":
self.logger.debug("Connecting TCP host=%s port=%d", self.args["host"], self.args["port"])
async with await connect_tcp(remote_host=self.args["host"], remote_port=self.args["port"]) as stream:
yield stream
@exportstream
@asynccontextmanager
async def connect(self):
try:
match self.scheme:
case "unix":
self.logger.debug("Connecting UDS path=%s", self.args["path"])
async with await connect_unix(path=self.args["path"]) as stream:
yield stream
case "tcp":
self.logger.debug("Connecting TCP host=%s port=%d", self.args["host"], self.args["port"])
async with await connect_tcp(remote_host=self.args["host"], remote_port=self.args["port"]) as stream:
yield stream
except OSError as e:
raise ConnectionError(f"Failed to connect to {self.kind} bus: {e}") from e


class EchoNetwork(NetworkInterface, Driver):
'''
EchoNetwork is a mock driver implementing the NetworkInterface
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import os
import socket
import subprocess
import sys
Expand All @@ -7,7 +8,7 @@
from anyio.from_thread import start_blocking_portal

from .adapters import TcpPortforwardAdapter, UnixPortforwardAdapter
from .driver import TcpNetwork, UdpNetwork, UnixNetwork
from .driver import DbusNetwork, TcpNetwork, UdpNetwork, UnixNetwork
from jumpstarter.common import TemporaryUnixListener
from jumpstarter.common.utils import serve

Expand Down Expand Up @@ -100,3 +101,43 @@ def test_tcp_network_performance():
)

server.terminate()


@pytest.mark.skipif(
os.getenv("DBUS_SYSTEM_BUS_ADDRESS") is None and not os.path.exists("/run/dbus/system_bus_socket"),
reason="dbus system bus not available",
)
@pytest.mark.skipif(which("busctl") is None, reason="busctl not available")
def test_dbus_network_system(monkeypatch):
with serve(DbusNetwork(kind="system")) as client:
assert client.kind == "system"
oldvar = os.getenv("DBUS_SYSTEM_BUS_ADDRESS")
with client:
assert oldvar != os.getenv("DBUS_SYSTEM_BUS_ADDRESS")
subprocess.run(
["busctl", "list", "--system", "--no-pager"],
check=True,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
)
assert oldvar == os.getenv("DBUS_SYSTEM_BUS_ADDRESS")


@pytest.mark.skipif(
os.getenv("DBUS_SESSION_BUS_ADDRESS") is None and not os.path.exists(f"/run/user/{os.getuid()}/bus"),
reason="dbus session bus not available",
)
@pytest.mark.skipif(which("busctl") is None, reason="busctl not available")
def test_dbus_network_session(monkeypatch):
with serve(DbusNetwork(kind="session")) as client:
assert client.kind == "session"
oldvar = os.getenv("DBUS_SESSION_BUS_ADDRESS")
with client:
assert oldvar != os.getenv("DBUS_SESSION_BUS_ADDRESS")
subprocess.run(
["busctl", "list", "--user", "--no-pager"],
check=True,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
)
assert oldvar == os.getenv("DBUS_SESSION_BUS_ADDRESS")
4 changes: 4 additions & 0 deletions packages/jumpstarter/jumpstarter/driver/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,9 @@ def client(cls) -> str:
Return full import path of the corresponding driver client class
"""

def extra_labels(self) -> dict[str, str]:
return {}

async def DriverCall(self, request, context):
"""
:meta private:
Expand Down Expand Up @@ -171,6 +174,7 @@ def report(self, *, parent=None, name=None):
uuid=str(self.uuid),
parent_uuid=str(parent.uuid) if parent else None,
labels=self.labels
| self.extra_labels()
| ({"jumpstarter.dev/client": self.client()})
| ({"jumpstarter.dev/name": name} if name else {}),
)
Expand Down
39 changes: 20 additions & 19 deletions uv.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.