diff --git a/packages/jumpstarter-driver-composite/jumpstarter_driver_composite/client.py b/packages/jumpstarter-driver-composite/jumpstarter_driver_composite/client.py
index 468fd9450..d41ff92be 100644
--- a/packages/jumpstarter-driver-composite/jumpstarter_driver_composite/client.py
+++ b/packages/jumpstarter-driver-composite/jumpstarter_driver_composite/client.py
@@ -5,6 +5,7 @@
from rich import traceback
from jumpstarter.client import DriverClient
+from jumpstarter.client.decorators import driver_click_group
def _opt_log_level_callback(ctx, param, value):
@@ -34,7 +35,7 @@ def close(self):
v.close()
def cli(self):
- @click.group
+ @driver_click_group(self)
@click.option(
"--log-level",
"log_level",
diff --git a/packages/jumpstarter-driver-flashers/jumpstarter_driver_flashers/client.py b/packages/jumpstarter-driver-flashers/jumpstarter_driver_flashers/client.py
index 1bfd8698d..123e0c005 100644
--- a/packages/jumpstarter-driver-flashers/jumpstarter_driver_flashers/client.py
+++ b/packages/jumpstarter-driver-flashers/jumpstarter_driver_flashers/client.py
@@ -21,6 +21,7 @@
from jumpstarter_driver_flashers.bundle import FlasherBundleManifestV1Alpha1
+from jumpstarter.client.decorators import driver_click_group
from jumpstarter.common.exceptions import ArgumentError
debug_console_option = click.option("--console-debug", is_flag=True, help="Enable console debug mode")
@@ -611,7 +612,7 @@ def manifest(self):
return self._manifest
def cli(self):
- @click.group
+ @driver_click_group(self)
def base():
"""Software-defined flasher interface"""
pass
diff --git a/packages/jumpstarter-driver-gpiod/jumpstarter_driver_gpiod/client.py b/packages/jumpstarter-driver-gpiod/jumpstarter_driver_gpiod/client.py
index df6ef0f90..3c30f5c54 100644
--- a/packages/jumpstarter-driver-gpiod/jumpstarter_driver_gpiod/client.py
+++ b/packages/jumpstarter-driver-gpiod/jumpstarter_driver_gpiod/client.py
@@ -5,6 +5,7 @@
from jumpstarter_driver_power.client import PowerClient
from jumpstarter.client import DriverClient
+from jumpstarter.client.decorators import driver_click_group
class PinState(Enum):
@@ -34,7 +35,7 @@ def read(self):
return PinState(int(self.call("read_pin")))
def cli(self):
- @click.group()
+ @driver_click_group(self)
def gpio():
"""GPIO power control commands."""
pass
@@ -79,7 +80,7 @@ def read(self):
return PinState(int(self.call("read_pin")))
def cli(self):
- @click.group()
+ @driver_click_group(self)
def gpio():
"""GPIO input commands."""
pass
diff --git a/packages/jumpstarter-driver-network/jumpstarter_driver_network/client.py b/packages/jumpstarter-driver-network/jumpstarter_driver_network/client.py
index 85befd4e1..35f6cd3ef 100644
--- a/packages/jumpstarter-driver-network/jumpstarter_driver_network/client.py
+++ b/packages/jumpstarter-driver-network/jumpstarter_driver_network/client.py
@@ -12,6 +12,7 @@
from .driver import DbusNetwork
from jumpstarter.client import DriverClient
from jumpstarter.client.core import DriverMethodNotImplemented
+from jumpstarter.client.decorators import driver_click_group
class NetworkClient(DriverClient):
@@ -20,7 +21,7 @@ def address(self):
return self.call("address")
def cli(self):
- @click.group
+ @driver_click_group(self)
def base():
"""Generic Network Connection"""
pass
diff --git a/packages/jumpstarter-driver-opendal/jumpstarter_driver_opendal/client.py b/packages/jumpstarter-driver-opendal/jumpstarter_driver_opendal/client.py
index 045484c5a..d56860ad1 100644
--- a/packages/jumpstarter-driver-opendal/jumpstarter_driver_opendal/client.py
+++ b/packages/jumpstarter-driver-opendal/jumpstarter_driver_opendal/client.py
@@ -19,6 +19,7 @@
from .adapter import OpendalAdapter
from .common import Capability, HashAlgo, Metadata, Mode, PathBuf, PresignedRequest
from jumpstarter.client import DriverClient
+from jumpstarter.client.decorators import driver_click_group
from jumpstarter.common.exceptions import ArgumentError
from jumpstarter.streams.encoding import Compression
@@ -413,9 +414,10 @@ def cli(self): # noqa: C901
arg_dst = click.argument("dst", type=click.Path())
opt_expire_second = click.option("--expire-second", type=int, required=True)
- @click.group
+ @driver_click_group(self)
def base():
"""Opendal Storage"""
+ pass
@base.command
@arg_path
@@ -548,7 +550,7 @@ def dump(
...
def cli(self):
- @click.group
+ @driver_click_group(self)
def base():
"""Generic flasher interface"""
pass
@@ -716,7 +718,10 @@ def read_local_file(self, filepath):
def cli(self, base=None):
if base is None:
- base = click.group(lambda: None)
+ @driver_click_group(self)
+ def base():
+ """Storage operations"""
+ pass
@base.command()
def host():
diff --git a/packages/jumpstarter-driver-power/jumpstarter_driver_power/client.py b/packages/jumpstarter-driver-power/jumpstarter_driver_power/client.py
index eb86b7b15..3fc25598e 100644
--- a/packages/jumpstarter-driver-power/jumpstarter_driver_power/client.py
+++ b/packages/jumpstarter-driver-power/jumpstarter_driver_power/client.py
@@ -5,6 +5,7 @@
from .common import PowerReading
from jumpstarter.client import DriverClient
+from jumpstarter.client.decorators import driver_click_group
class PowerClient(DriverClient):
@@ -35,7 +36,7 @@ def read(self) -> Generator[PowerReading, None, None]:
yield PowerReading.model_validate(v, strict=True)
def cli(self):
- @click.group
+ @driver_click_group(self)
def base():
"""Generic power"""
pass
diff --git a/packages/jumpstarter-driver-probe-rs/jumpstarter_driver_probe_rs/client.py b/packages/jumpstarter-driver-probe-rs/jumpstarter_driver_probe_rs/client.py
index 126276fc0..6f6bbcf69 100644
--- a/packages/jumpstarter-driver-probe-rs/jumpstarter_driver_probe_rs/client.py
+++ b/packages/jumpstarter-driver-probe-rs/jumpstarter_driver_probe_rs/client.py
@@ -6,6 +6,7 @@
from opendal import Operator
from jumpstarter.client import DriverClient
+from jumpstarter.client.decorators import driver_click_group
from jumpstarter.common.exceptions import ArgumentError
@@ -60,7 +61,7 @@ def read(self, width: int, address: int, words: int) -> list[int]:
return [int(data, 16) for data in data_strs]
def cli(self): # noqa: C901
- @click.group
+ @driver_click_group(self)
def base():
"""probe-rs client"""
pass
diff --git a/packages/jumpstarter-driver-pyserial/jumpstarter_driver_pyserial/client.py b/packages/jumpstarter-driver-pyserial/jumpstarter_driver_pyserial/client.py
index 523168826..7a4f643eb 100644
--- a/packages/jumpstarter-driver-pyserial/jumpstarter_driver_pyserial/client.py
+++ b/packages/jumpstarter-driver-pyserial/jumpstarter_driver_pyserial/client.py
@@ -6,6 +6,7 @@
from .console import Console
from jumpstarter.client import DriverClient
+from jumpstarter.client.decorators import driver_click_group
class PySerialClient(DriverClient):
@@ -36,7 +37,7 @@ def pexpect(self):
yield adapter
def cli(self):
- @click.group
+ @driver_click_group(self)
def base():
"""Serial port client"""
pass
diff --git a/packages/jumpstarter-driver-ridesx/jumpstarter_driver_ridesx/client.py b/packages/jumpstarter-driver-ridesx/jumpstarter_driver_ridesx/client.py
index f9a619647..978ba2ec0 100644
--- a/packages/jumpstarter-driver-ridesx/jumpstarter_driver_ridesx/client.py
+++ b/packages/jumpstarter-driver-ridesx/jumpstarter_driver_ridesx/client.py
@@ -2,12 +2,13 @@
from pathlib import Path
from typing import Dict, Optional
-import click
from jumpstarter_driver_composite.client import CompositeClient
from jumpstarter_driver_opendal.client import FlasherClient, operator_for_path
from jumpstarter_driver_power.client import PowerClient
from opendal import Operator
+from jumpstarter.client.decorators import driver_click_group
+
PROMPT = "CMD >> "
@@ -102,8 +103,9 @@ def flash(
def cli(self):
generic_cli = FlasherClient.cli(self)
- @click.group()
+ @driver_click_group(self)
def storage():
+ """RideSX storage operations"""
pass
for name, cmd in generic_cli.commands.items():
diff --git a/packages/jumpstarter-driver-shell/README.md b/packages/jumpstarter-driver-shell/README.md
index d80f497bf..aa552cec5 100644
--- a/packages/jumpstarter-driver-shell/README.md
+++ b/packages/jumpstarter-driver-shell/README.md
@@ -11,7 +11,9 @@ $ pip3 install --extra-index-url {{index_url}} jumpstarter-driver-shell
## Configuration
-Example configuration:
+The shell driver supports two configuration formats for methods:
+
+### Format 1: Simple String e.g. for self-descriptive short commands
```yaml
export:
@@ -20,12 +22,40 @@ export:
config:
methods:
ls: "ls"
- method2: "echo 'Hello World 2'"
- #multi line method
- method3: |
- echo 'Hello World $1'
- echo 'Hello World $2'
- env_var: "echo $1,$2,$ENV_VAR"
+ echo_hello: "echo 'Hello World'"
+```
+
+### Format 2: Unified Format with Descriptions
+
+```yaml
+export:
+ shell:
+ type: jumpstarter_driver_shell.driver.Shell
+ config:
+ methods:
+ ls:
+ command: "ls -la"
+ description: "List directory contents with details"
+ deploy:
+ command: "ansible-playbook deploy.yml"
+ description: "Deploy application using Ansible"
+ # Multi-line commands work too
+ setup:
+ command: |
+ echo 'Setting up environment'
+ export PATH=$PATH:/usr/local/bin
+ ./setup.sh
+ description: "Set up the development environment"
+ # Description-only (uses default "echo Hello" command)
+ placeholder:
+ description: "Placeholder method for testing"
+ # Custom timeout for long-running operations
+ long_backup:
+ command: "tar -czf backup.tar.gz /data && rsync backup.tar.gz remote:/backups/"
+ description: "Create and sync backup (may take a while)"
+ timeout: 1800 # 30 minutes instead of default 5 minutes
+ # You can mix both formats
+ simple_echo: "echo 'simple'"
# optional parameters
cwd: "/tmp"
log_level: "INFO"
@@ -34,6 +64,25 @@ export:
- "-c"
```
+### Configuration Parameters
+
+| Parameter | Description | Type | Required | Default |
+|-----------|-------------|------|----------|---------|
+| `methods` | Dictionary of methods. Values can be:
- String: just the command
- Dict: `{command: "...", description: "...", timeout: ...}` | `dict[str, str \| dict]` | Yes | - |
+| `cwd` | Working directory for shell commands | `str` | No | `None` |
+| `log_level` | Logging level | `str` | No | `"INFO"` |
+| `shell` | Shell command to execute scripts | `list[str]` | No | `["bash", "-c"]` |
+| `timeout` | Command timeout in seconds | `int` | No | `300` |
+
+**Method Configuration Options:**
+
+For the dict format, each method supports:
+- `command`: The shell command to execute (optional, defaults to `"echo Hello"`)
+- `description`: CLI help text (optional, defaults to `"Execute the {method_name} shell method"`)
+- `timeout`: Command-specific timeout in seconds (optional, defaults to global `timeout` value)
+
+**Note:** You can mix both formats in the same configuration - use string format for simple commands and dict format when you want custom descriptions or timeouts.
+
## API Reference
Assuming the exporter driver is configured as in the example above, the client
@@ -66,7 +115,11 @@ methods will be generated dynamically, and they will be available as follows:
## CLI Usage
-The shell driver also provides a CLI when using `jmp shell`. All configured methods become available as CLI commands, except for methods starting with `_` which are considered private and hidden from the end user:
+The shell driver also provides a CLI when using `jmp shell`. All configured methods become available as CLI commands, except for methods starting with `_` which are considered private and hidden from the end user.
+
+### CLI Help Output
+
+With unified format (custom descriptions):
```console
$ jmp shell --exporter shell-exporter
@@ -76,10 +129,40 @@ Usage: j shell [OPTIONS] COMMAND [ARGS]...
Shell command executor
Commands:
- env_var Execute the env_var shell method
- ls Execute the ls shell method
- method2 Execute the method2 shell method
- method3 Execute the method3 shell method
+ deploy Deploy application using Ansible
+ ls List directory contents with details
+ setup Set up the development environment
+```
+
+With simple string format (default descriptions):
+
+```console
+$ j shell
+Usage: j shell [OPTIONS] COMMAND [ARGS]...
+
+ Shell command executor
+
+Commands:
+ deploy Execute the deploy shell method
+ ls Execute the ls shell method
+ setup Execute the setup shell method
+```
+
+**Mixed format example:**
+
+```yaml
+methods:
+ deploy:
+ command: "ansible-playbook deploy.yml"
+ description: "Deploy using Ansible"
+ restart: "systemctl restart myapp" # Simple format
+```
+
+Results in:
+```console
+Commands:
+ deploy Deploy using Ansible
+ restart Execute the restart shell method
```
### CLI Command Usage
diff --git a/packages/jumpstarter-driver-shell/jumpstarter_driver_shell/client.py b/packages/jumpstarter-driver-shell/jumpstarter_driver_shell/client.py
index ef8cee2f6..3f02905b5 100644
--- a/packages/jumpstarter-driver-shell/jumpstarter_driver_shell/client.py
+++ b/packages/jumpstarter-driver-shell/jumpstarter_driver_shell/client.py
@@ -4,6 +4,7 @@
import click
from jumpstarter.client import DriverClient
+from jumpstarter.client.decorators import driver_click_group
@dataclass(kw_only=True)
@@ -41,7 +42,7 @@ def execute(*args, **kwargs):
def cli(self):
"""Create CLI interface for dynamically configured shell methods"""
- @click.group
+ @driver_click_group(self)
def base():
"""Shell command executor"""
pass
@@ -58,13 +59,6 @@ def base():
def _add_method_command(self, group, method_name):
"""Add a Click command for a specific shell method"""
- @group.command(
- name=method_name,
- context_settings={"ignore_unknown_options": True, "allow_interspersed_args": False},
- )
- @click.argument('args', nargs=-1, type=click.UNPROCESSED)
- @click.option('--env', '-e', multiple=True,
- help='Environment variables in KEY=VALUE format')
def method_command(args, env):
# Parse environment variables
env_dict = {}
@@ -81,5 +75,12 @@ def method_command(args, env):
if returncode != 0:
raise click.exceptions.Exit(returncode)
- # Update the docstring dynamically
- method_command.__doc__ = f"Execute the {method_name} shell method"
+ # Decorate and register the command with help text
+ method_command = click.argument('args', nargs=-1, type=click.UNPROCESSED)(method_command)
+ method_command = click.option('--env', '-e', multiple=True,
+ help='Environment variables in KEY=VALUE format')(method_command)
+ method_command = group.command(
+ name=method_name,
+ help=self.methods_description.get( method_name, f"Execute the {method_name} shell method"),
+ context_settings={"ignore_unknown_options": True, "allow_interspersed_args": False},
+ )(method_command)
diff --git a/packages/jumpstarter-driver-shell/jumpstarter_driver_shell/driver.py b/packages/jumpstarter-driver-shell/jumpstarter_driver_shell/driver.py
index f790a30f2..d3bd613b3 100644
--- a/packages/jumpstarter-driver-shell/jumpstarter_driver_shell/driver.py
+++ b/packages/jumpstarter-driver-shell/jumpstarter_driver_shell/driver.py
@@ -12,13 +12,36 @@
class Shell(Driver):
"""shell driver for Jumpstarter"""
- # methods field is used to define the methods exported, and the shell script
- # to be executed by each method
- methods: dict[str, str]
+ # methods field defines the methods exported and their shell scripts
+ # Supports two formats:
+ # 1. Simple string: method_name: "command"
+ # 2. Dict with description: method_name: {command: "...", description: "...", timeout: ...}
+ methods: dict[str, str | dict[str, str | int]]
shell: list[str] = field(default_factory=lambda: ["bash", "-c"])
timeout: int = 300
cwd: str | None = None
+ def __post_init__(self):
+ super().__post_init__()
+ # Extract descriptions from methods configuration and populate methods_description
+ for method_name, method_config in self.methods.items():
+ if isinstance(method_config, dict) and "description" in method_config:
+ self.methods_description[method_name] = method_config["description"]
+
+ def _get_method_command(self, method: str) -> str:
+ """Extract the command string from a method configuration"""
+ method_config = self.methods[method]
+ if isinstance(method_config, str):
+ return method_config
+ return method_config.get("command", "echo Hello")
+
+ def _get_method_timeout(self, method: str) -> int:
+ """Extract the timeout from a method configuration, fallback to global timeout"""
+ method_config = self.methods[method]
+ if isinstance(method_config, str):
+ return self.timeout
+ return method_config.get("timeout", self.timeout)
+
@classmethod
def client(cls) -> str:
return "jumpstarter_driver_shell.client.ShellClient"
@@ -39,12 +62,13 @@ async def call_method(self, method: str, env, *args) -> AsyncGenerator[tuple[str
self.logger.info(f"calling {method} with args: {args} and kwargs as env: {env}")
if method not in self.methods:
raise ValueError(f"Method '{method}' not found in available methods: {list(self.methods.keys())}")
- script = self.methods[method]
- self.logger.debug(f"running script: {script}")
+ script = self._get_method_command(method)
+ timeout = self._get_method_timeout(method)
+ self.logger.debug(f"running script: {script} with timeout: {timeout}")
try:
async for stdout_chunk, stderr_chunk, returncode in self._run_inline_shell_script(
- method, script, *args, env_vars=env
+ method, script, *args, env_vars=env, timeout=timeout
):
if stdout_chunk:
self.logger.debug(f"{method} stdout:\n{stdout_chunk.rstrip()}")
@@ -121,7 +145,7 @@ async def _read_process_output(self, process, read_all=False):
return stdout_data, stderr_data
async def _run_inline_shell_script(
- self, method, script, *args, env_vars=None
+ self, method, script, *args, env_vars=None, timeout=None
) -> AsyncGenerator[tuple[str, str, int | None], None]:
"""
Run the given shell script with live streaming output.
@@ -130,6 +154,7 @@ async def _run_inline_shell_script(
:param script: The shell script contents as a string.
:param args: Arguments to pass to the script (mapped to $1, $2, etc. in the script).
:param env_vars: A dict of environment variables to make available to the script.
+ :param timeout: Customized command timeout in seconds. If None, uses global timeout.
:yields: Tuples of (stdout_chunk, stderr_chunk, returncode).
returncode is None until the process completes.
@@ -151,9 +176,12 @@ async def _run_inline_shell_script(
# Create a task to monitor the process timeout
start_time = asyncio.get_event_loop().time()
+ if timeout is None:
+ timeout = self.timeout
+
# Read output in real-time
while process.returncode is None:
- if asyncio.get_event_loop().time() - start_time > self.timeout:
+ if asyncio.get_event_loop().time() - start_time > timeout:
# Send SIGTERM to entire process group for graceful termination
try:
os.killpg(process.pid, signal.SIGTERM)
@@ -168,7 +196,7 @@ async def _run_inline_shell_script(
self.logger.warning(f"SIGTERM failed to terminate {process.pid}, sending SIGKILL")
except (ProcessLookupError, OSError):
pass
- raise subprocess.TimeoutExpired(cmd, self.timeout) from None
+ raise subprocess.TimeoutExpired(cmd, timeout) from None
try:
stdout_data, stderr_data = await self._read_process_output(process, read_all=False)
diff --git a/packages/jumpstarter-driver-shell/jumpstarter_driver_shell/driver_test.py b/packages/jumpstarter-driver-shell/jumpstarter_driver_shell/driver_test.py
index 4e03b5f64..20faad63d 100644
--- a/packages/jumpstarter-driver-shell/jumpstarter_driver_shell/driver_test.py
+++ b/packages/jumpstarter-driver-shell/jumpstarter_driver_shell/driver_test.py
@@ -101,74 +101,157 @@ def test_cli_method_execution(client):
"""Test that CLI methods can be executed"""
cli = client.cli()
- # Test that we can get the echo command
- echo_command = cli.commands.get('echo')
- assert echo_command is not None
+ # Test that CLI commands exist and have the correct structure
+ echo_command = cli.commands['echo']
assert echo_command.name == 'echo'
def test_cli_includes_all_methods():
- """Test that CLI includes all methods"""
- from .driver import Shell
- from jumpstarter.common.utils import serve
-
- shell_instance = Shell(
- log_level="DEBUG",
+ """Test that CLI includes all configured methods with proper names"""
+ shell = Shell(
methods={
- "method1": "echo method1",
- "method2": "echo method2",
- "method3": "echo method3",
- },
+ "echo": "echo",
+ "env": "echo $ENV_VAR",
+ "multi_line": "echo line1\necho line2",
+ "exit1": "exit 1",
+ "stderr": "echo 'error' 1>&2",
+ }
)
- with serve(shell_instance) as test_client:
- cli = test_client.cli()
+ with serve(shell) as client:
+ cli = client.cli()
+
+ # Check that all methods are in the CLI
+ expected_methods = {"echo", "env", "multi_line", "exit1", "stderr"}
available_commands = set(cli.commands.keys())
- # All methods should be available
- expected_methods = {"method1", "method2", "method3"}
assert available_commands == expected_methods, f"Expected {expected_methods}, got {available_commands}"
def test_cli_exit_codes():
- """Test that CLI commands preserve shell command exit codes"""
- import click
+ """Test that CLI methods correctly exit with shell command return codes"""
+ shell = Shell(
+ methods={
+ "exit0": "exit 0",
+ "exit1": "exit 1",
+ "exit42": "exit 42",
+ }
+ )
- from .driver import Shell
- from jumpstarter.common.utils import serve
+ with serve(shell) as client:
+ # Test successful command (exit 0)
+ returncode = client.exit0()
+ assert returncode == 0
- # Create a shell instance with methods that have different exit codes
- shell_instance = Shell(
- log_level="DEBUG",
+ # Test failed command (exit 1)
+ returncode = client.exit1()
+ assert returncode == 1
+
+ # Test custom exit code (exit 42)
+ returncode = client.exit42()
+ assert returncode == 42
+
+
+def test_cli_custom_descriptions_unified_format():
+ """Test that CLI methods use custom descriptions with unified format"""
+ shell = Shell(
methods={
- "success": "exit 0",
- "fail_1": "exit 1",
- "fail_42": "exit 42",
- },
+ "echo": {
+ "command": "echo",
+ "description": "Custom echo description"
+ },
+ "test_method": {
+ "command": "echo 'test'",
+ "description": "Test method with custom description"
+ },
+ }
+ )
+
+ with serve(shell) as client:
+ cli = client.cli()
+
+ # Check that custom descriptions are used
+ echo_cmd = cli.commands['echo']
+ assert echo_cmd.help == "Custom echo description", f"Expected custom description, got {echo_cmd.help}"
+
+ test_cmd = cli.commands['test_method']
+ assert test_cmd.help == "Test method with custom description"
+
+
+def test_cli_default_descriptions():
+ """Test that CLI methods use default descriptions when not configured"""
+ shell = Shell(
+ methods={
+ "echo": "echo",
+ "test_method": "echo 'test'",
+ }
+ # No descriptions configured
+ )
+
+ with serve(shell) as client:
+ cli = client.cli()
+
+ # Check that default descriptions are used
+ echo_cmd = cli.commands['echo']
+ assert echo_cmd.help == "Execute the echo shell method"
+
+ test_cmd = cli.commands['test_method']
+ assert test_cmd.help == "Execute the test_method shell method"
+
+
+def test_methods_description_populated():
+ """Test that methods_description is populated from methods configuration"""
+ shell = Shell(
+ methods={
+ "method1": {
+ "command": "echo",
+ "description": "Custom description for method1"
+ },
+ "method2": "ls", # String format, no description in methods_description
+ }
)
- with serve(shell_instance) as test_client:
- cli = test_client.cli()
-
- # Test successful command (exit 0) - should not raise
- success_cmd = cli.commands['success']
- try:
- success_cmd.callback([], []) # Call with empty args and env
- except click.exceptions.Exit:
- raise AssertionError("Success command should not raise Exit exception") from None
-
- # Test command that exits with code 1 - should raise Exit(1)
- fail1_cmd = cli.commands['fail_1']
- try:
- fail1_cmd.callback([], [])
- raise AssertionError("Command should have raised Exit exception")
- except click.exceptions.Exit as e:
- assert e.exit_code == 1
-
- # Test command that exits with code 42 - should raise Exit(42)
- fail42_cmd = cli.commands['fail_42']
- try:
- fail42_cmd.callback([], [])
- raise AssertionError("Command should have raised Exit exception")
- except click.exceptions.Exit as e:
- assert e.exit_code == 42
+ with serve(shell) as client:
+ # Test that custom descriptions are in methods_description
+ assert "method1" in client.methods_description
+ assert client.methods_description["method1"] == "Custom description for method1"
+
+ # Test that string-format methods are not in methods_description
+ # (will fall back to default in client)
+ assert "method2" not in client.methods_description
+
+
+def test_mixed_format_methods():
+ """Test that both string and dict formats work together"""
+ shell = Shell(
+ methods={
+ "simple": "echo 'simple'",
+ "detailed": {
+ "command": "echo 'detailed'",
+ "description": "A detailed command with description"
+ },
+ "default_cmd": {
+ # No command specified - should use default "echo Hello"
+ "description": "Method using default command"
+ }
+ }
+ )
+
+ with serve(shell) as client:
+ # Test string format works
+ returncode = client.simple()
+ assert returncode == 0
+
+ # Test dict format works
+ returncode = client.detailed()
+ assert returncode == 0
+
+ # Test default command works
+ returncode = client.default_cmd()
+ assert returncode == 0
+
+ # Test CLI descriptions
+ cli = client.cli()
+ assert cli.commands['simple'].help == "Execute the simple shell method"
+ assert cli.commands['detailed'].help == "A detailed command with description"
+ assert cli.commands['default_cmd'].help == "Method using default command"
diff --git a/packages/jumpstarter-driver-snmp/jumpstarter_driver_snmp/client.py b/packages/jumpstarter-driver-snmp/jumpstarter_driver_snmp/client.py
index 6eb84afa8..17777e46f 100644
--- a/packages/jumpstarter-driver-snmp/jumpstarter_driver_snmp/client.py
+++ b/packages/jumpstarter-driver-snmp/jumpstarter_driver_snmp/client.py
@@ -1,8 +1,9 @@
from dataclasses import dataclass
-import click
from jumpstarter_driver_power.client import PowerClient
+from jumpstarter.client.decorators import driver_click_group
+
@dataclass(kw_only=True)
class SNMPServerClient(PowerClient):
@@ -17,7 +18,7 @@ def off(self):
self.call("off")
def cli(self):
- @click.group()
+ @driver_click_group(self)
def snmp():
"""SNMP power control commands"""
pass
diff --git a/packages/jumpstarter-driver-ssh/jumpstarter_driver_ssh/client.py b/packages/jumpstarter-driver-ssh/jumpstarter_driver_ssh/client.py
index c31e36ac8..00ee792a7 100644
--- a/packages/jumpstarter-driver-ssh/jumpstarter_driver_ssh/client.py
+++ b/packages/jumpstarter-driver-ssh/jumpstarter_driver_ssh/client.py
@@ -8,6 +8,7 @@
from jumpstarter_driver_network.adapters import TcpPortforwardAdapter
from jumpstarter.client.core import DriverMethodNotImplemented
+from jumpstarter.client.decorators import driver_click_command
@dataclass(kw_only=True)
@@ -19,11 +20,14 @@ class SSHWrapperClient(CompositeClient):
"""
def cli(self):
- @click.command(context_settings={"ignore_unknown_options": True})
+ @driver_click_command(
+ self,
+ context_settings={"ignore_unknown_options": True},
+ help="Run SSH command with arguments",
+ )
@click.option("--direct", is_flag=True, help="Use direct TCP address")
@click.argument("args", nargs=-1)
def ssh(direct, args):
- """Run SSH command with arguments"""
result = self.run(direct, args)
self.logger.debug(f"SSH result: {result}")
if result != 0:
diff --git a/packages/jumpstarter-driver-tmt/jumpstarter_driver_tmt/client.py b/packages/jumpstarter-driver-tmt/jumpstarter_driver_tmt/client.py
index b71490659..539a57e88 100644
--- a/packages/jumpstarter-driver-tmt/jumpstarter_driver_tmt/client.py
+++ b/packages/jumpstarter-driver-tmt/jumpstarter_driver_tmt/client.py
@@ -7,6 +7,7 @@
from jumpstarter_driver_network.adapters import TcpPortforwardAdapter
from jumpstarter.client.core import DriverMethodNotImplemented
+from jumpstarter.client.decorators import driver_click_command
def redact_password_in_args(args):
@@ -33,7 +34,11 @@ class TMTClient(CompositeClient):
def cli(self):
- @click.command(context_settings={"ignore_unknown_options": True})
+ @driver_click_command(
+ self,
+ context_settings={"ignore_unknown_options": True},
+ help="Run TMT command with arguments",
+ )
@click.option("--forward-ssh", is_flag=True)
@click.option("--tmt-username", default=None)
@click.option("--tmt-password", default=None)
@@ -41,7 +46,6 @@ def cli(self):
@click.option("--tmt-on-exporter", is_flag=True)
@click.argument("args", nargs=-1)
def tmt(forward_ssh, tmt_username, tmt_password, tmt_cmd, tmt_on_exporter, args):
- """Run TMT command with arguments"""
if tmt_on_exporter:
click.echo("TMT will be run on the exporter")
raise click.Abort("Still not implemented")
diff --git a/packages/jumpstarter-protocol/jumpstarter_protocol/jumpstarter/client/v1/client_pb2.py b/packages/jumpstarter-protocol/jumpstarter_protocol/jumpstarter/client/v1/client_pb2.py
index 9682b4b6b..be56f7f16 100644
--- a/packages/jumpstarter-protocol/jumpstarter_protocol/jumpstarter/client/v1/client_pb2.py
+++ b/packages/jumpstarter-protocol/jumpstarter_protocol/jumpstarter/client/v1/client_pb2.py
@@ -31,22 +31,24 @@
from google.protobuf import field_mask_pb2 as google_dot_protobuf_dot_field__mask__pb2
from google.protobuf import timestamp_pb2 as google_dot_protobuf_dot_timestamp__pb2
from ...v1 import kubernetes_pb2 as jumpstarter_dot_v1_dot_kubernetes__pb2
+from ...v1 import common_pb2 as jumpstarter_dot_v1_dot_common__pb2
-DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\"jumpstarter/client/v1/client.proto\x12\x15jumpstarter.client.v1\x1a\x1cgoogle/api/annotations.proto\x1a\x17google/api/client.proto\x1a\x1fgoogle/api/field_behavior.proto\x1a\x19google/api/resource.proto\x1a\x1egoogle/protobuf/duration.proto\x1a\x1bgoogle/protobuf/empty.proto\x1a google/protobuf/field_mask.proto\x1a\x1fgoogle/protobuf/timestamp.proto\x1a\x1fjumpstarter/v1/kubernetes.proto\"\xa1\x02\n\x08\x45xporter\x12\x17\n\x04name\x18\x01 \x01(\tB\x03\xe0\x41\x08R\x04name\x12\x43\n\x06labels\x18\x02 \x03(\x0b\x32+.jumpstarter.client.v1.Exporter.LabelsEntryR\x06labels\x12\x1b\n\x06online\x18\x03 \x01(\x08\x42\x03\xe0\x41\x03R\x06online\x1a\x39\n\x0bLabelsEntry\x12\x10\n\x03key\x18\x01 \x01(\tR\x03key\x12\x14\n\x05value\x18\x02 \x01(\tR\x05value:\x02\x38\x01:_\xea\x41\\\n\x18jumpstarter.dev/Exporter\x12+namespaces/{namespace}/exporters/{exporter}*\texporters2\x08\x65xporter\"\xed\x06\n\x05Lease\x12\x17\n\x04name\x18\x01 \x01(\tB\x03\xe0\x41\x08R\x04name\x12\"\n\x08selector\x18\x02 \x01(\tB\x06\xe0\x41\x02\xe0\x41\x05R\x08selector\x12:\n\x08\x64uration\x18\x03 \x01(\x0b\x32\x19.google.protobuf.DurationB\x03\xe0\x41\x02R\x08\x64uration\x12M\n\x12\x65\x66\x66\x65\x63tive_duration\x18\x04 \x01(\x0b\x32\x19.google.protobuf.DurationB\x03\xe0\x41\x03R\x11\x65\x66\x66\x65\x63tiveDuration\x12>\n\nbegin_time\x18\x05 \x01(\x0b\x32\x1a.google.protobuf.TimestampH\x00R\tbeginTime\x88\x01\x01\x12V\n\x14\x65\x66\x66\x65\x63tive_begin_time\x18\x06 \x01(\x0b\x32\x1a.google.protobuf.TimestampB\x03\xe0\x41\x03H\x01R\x12\x65\x66\x66\x65\x63tiveBeginTime\x88\x01\x01\x12:\n\x08\x65nd_time\x18\x07 \x01(\x0b\x32\x1a.google.protobuf.TimestampH\x02R\x07\x65ndTime\x88\x01\x01\x12R\n\x12\x65\x66\x66\x65\x63tive_end_time\x18\x08 \x01(\x0b\x32\x1a.google.protobuf.TimestampB\x03\xe0\x41\x03H\x03R\x10\x65\x66\x66\x65\x63tiveEndTime\x88\x01\x01\x12;\n\x06\x63lient\x18\t \x01(\tB\x1e\xe0\x41\x03\xfa\x41\x18\n\x16jumpstarter.dev/ClientH\x04R\x06\x63lient\x88\x01\x01\x12\x41\n\x08\x65xporter\x18\n \x01(\tB \xe0\x41\x03\xfa\x41\x1a\n\x18jumpstarter.dev/ExporterH\x05R\x08\x65xporter\x88\x01\x01\x12>\n\nconditions\x18\x0b \x03(\x0b\x32\x19.jumpstarter.v1.ConditionB\x03\xe0\x41\x03R\nconditions:P\xea\x41M\n\x15jumpstarter.dev/Lease\x12%namespaces/{namespace}/leases/{lease}*\x06leases2\x05leaseB\r\n\x0b_begin_timeB\x17\n\x15_effective_begin_timeB\x0b\n\t_end_timeB\x15\n\x13_effective_end_timeB\t\n\x07_clientB\x0b\n\t_exporter\"J\n\x12GetExporterRequest\x12\x34\n\x04name\x18\x01 \x01(\tB \xe0\x41\x02\xfa\x41\x1a\n\x18jumpstarter.dev/ExporterR\x04name\"\xb3\x01\n\x14ListExportersRequest\x12\x38\n\x06parent\x18\x01 \x01(\tB \xe0\x41\x02\xfa\x41\x1a\x12\x18jumpstarter.dev/ExporterR\x06parent\x12 \n\tpage_size\x18\x02 \x01(\x05\x42\x03\xe0\x41\x01R\x08pageSize\x12\"\n\npage_token\x18\x03 \x01(\tB\x03\xe0\x41\x01R\tpageToken\x12\x1b\n\x06\x66ilter\x18\x04 \x01(\tB\x03\xe0\x41\x01R\x06\x66ilter\"~\n\x15ListExportersResponse\x12=\n\texporters\x18\x01 \x03(\x0b\x32\x1f.jumpstarter.client.v1.ExporterR\texporters\x12&\n\x0fnext_page_token\x18\x02 \x01(\tR\rnextPageToken\"D\n\x0fGetLeaseRequest\x12\x31\n\x04name\x18\x01 \x01(\tB\x1d\xe0\x41\x02\xfa\x41\x17\n\x15jumpstarter.dev/LeaseR\x04name\"\xad\x01\n\x11ListLeasesRequest\x12\x35\n\x06parent\x18\x01 \x01(\tB\x1d\xe0\x41\x02\xfa\x41\x17\x12\x15jumpstarter.dev/LeaseR\x06parent\x12 \n\tpage_size\x18\x02 \x01(\x05\x42\x03\xe0\x41\x01R\x08pageSize\x12\"\n\npage_token\x18\x03 \x01(\tB\x03\xe0\x41\x01R\tpageToken\x12\x1b\n\x06\x66ilter\x18\x04 \x01(\tB\x03\xe0\x41\x01R\x06\x66ilter\"r\n\x12ListLeasesResponse\x12\x34\n\x06leases\x18\x01 \x03(\x0b\x32\x1c.jumpstarter.client.v1.LeaseR\x06leases\x12&\n\x0fnext_page_token\x18\x02 \x01(\tR\rnextPageToken\"\xa4\x01\n\x12\x43reateLeaseRequest\x12\x35\n\x06parent\x18\x01 \x01(\tB\x1d\xe0\x41\x02\xfa\x41\x17\x12\x15jumpstarter.dev/LeaseR\x06parent\x12\x1e\n\x08lease_id\x18\x02 \x01(\tB\x03\xe0\x41\x01R\x07leaseId\x12\x37\n\x05lease\x18\x03 \x01(\x0b\x32\x1c.jumpstarter.client.v1.LeaseB\x03\xe0\x41\x02R\x05lease\"\x8f\x01\n\x12UpdateLeaseRequest\x12\x37\n\x05lease\x18\x01 \x01(\x0b\x32\x1c.jumpstarter.client.v1.LeaseB\x03\xe0\x41\x02R\x05lease\x12@\n\x0bupdate_mask\x18\x02 \x01(\x0b\x32\x1a.google.protobuf.FieldMaskB\x03\xe0\x41\x01R\nupdateMask\"G\n\x12\x44\x65leteLeaseRequest\x12\x31\n\x04name\x18\x01 \x01(\tB\x1d\xe0\x41\x02\xfa\x41\x17\n\x15jumpstarter.dev/LeaseR\x04name2\xa7\x08\n\rClientService\x12\x8d\x01\n\x0bGetExporter\x12).jumpstarter.client.v1.GetExporterRequest\x1a\x1f.jumpstarter.client.v1.Exporter\"2\xda\x41\x04name\x82\xd3\xe4\x93\x02%\x12#/v1/{name=namespaces/*/exporters/*}\x12\xa0\x01\n\rListExporters\x12+.jumpstarter.client.v1.ListExportersRequest\x1a,.jumpstarter.client.v1.ListExportersResponse\"4\xda\x41\x06parent\x82\xd3\xe4\x93\x02%\x12#/v1/{parent=namespaces/*}/exporters\x12\x81\x01\n\x08GetLease\x12&.jumpstarter.client.v1.GetLeaseRequest\x1a\x1c.jumpstarter.client.v1.Lease\"/\xda\x41\x04name\x82\xd3\xe4\x93\x02\"\x12 /v1/{name=namespaces/*/leases/*}\x12\x94\x01\n\nListLeases\x12(.jumpstarter.client.v1.ListLeasesRequest\x1a).jumpstarter.client.v1.ListLeasesResponse\"1\xda\x41\x06parent\x82\xd3\xe4\x93\x02\"\x12 /v1/{parent=namespaces/*}/leases\x12\x9f\x01\n\x0b\x43reateLease\x12).jumpstarter.client.v1.CreateLeaseRequest\x1a\x1c.jumpstarter.client.v1.Lease\"G\xda\x41\x15parent,lease,lease_id\x82\xd3\xe4\x93\x02)\" /v1/{parent=namespaces/*}/leases:\x05lease\x12\xa1\x01\n\x0bUpdateLease\x12).jumpstarter.client.v1.UpdateLeaseRequest\x1a\x1c.jumpstarter.client.v1.Lease\"I\xda\x41\x11lease,update_mask\x82\xd3\xe4\x93\x02/2&/v1/{lease.name=namespaces/*/leases/*}:\x05lease\x12\x81\x01\n\x0b\x44\x65leteLease\x12).jumpstarter.client.v1.DeleteLeaseRequest\x1a\x16.google.protobuf.Empty\"/\xda\x41\x04name\x82\xd3\xe4\x93\x02\"* /v1/{name=namespaces/*/leases/*}B\x9e\x01\n\x19\x63om.jumpstarter.client.v1B\x0b\x43lientProtoP\x01\xa2\x02\x03JCX\xaa\x02\x15Jumpstarter.Client.V1\xca\x02\x15Jumpstarter\\Client\\V1\xe2\x02!Jumpstarter\\Client\\V1\\GPBMetadata\xea\x02\x17Jumpstarter::Client::V1b\x06proto3')
+DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\"jumpstarter/client/v1/client.proto\x12\x15jumpstarter.client.v1\x1a\x1cgoogle/api/annotations.proto\x1a\x17google/api/client.proto\x1a\x1fgoogle/api/field_behavior.proto\x1a\x19google/api/resource.proto\x1a\x1egoogle/protobuf/duration.proto\x1a\x1bgoogle/protobuf/empty.proto\x1a google/protobuf/field_mask.proto\x1a\x1fgoogle/protobuf/timestamp.proto\x1a\x1fjumpstarter/v1/kubernetes.proto\x1a\x1bjumpstarter/v1/common.proto\"\xe0\x02\n\x08\x45xporter\x12\x17\n\x04name\x18\x01 \x01(\tB\x03\xe0\x41\x08R\x04name\x12\x43\n\x06labels\x18\x02 \x03(\x0b\x32+.jumpstarter.client.v1.Exporter.LabelsEntryR\x06labels\x12\x1d\n\x06online\x18\x03 \x01(\x08\x42\x05\x18\x01\xe0\x41\x03R\x06online\x12;\n\x06status\x18\x04 \x01(\x0e\x32\x1e.jumpstarter.v1.ExporterStatusB\x03\xe0\x41\x03R\x06status\x1a\x39\n\x0bLabelsEntry\x12\x10\n\x03key\x18\x01 \x01(\tR\x03key\x12\x14\n\x05value\x18\x02 \x01(\tR\x05value:\x02\x38\x01:_\xea\x41\\\n\x18jumpstarter.dev/Exporter\x12+namespaces/{namespace}/exporters/{exporter}*\texporters2\x08\x65xporter\"\xed\x06\n\x05Lease\x12\x17\n\x04name\x18\x01 \x01(\tB\x03\xe0\x41\x08R\x04name\x12\"\n\x08selector\x18\x02 \x01(\tB\x06\xe0\x41\x02\xe0\x41\x05R\x08selector\x12:\n\x08\x64uration\x18\x03 \x01(\x0b\x32\x19.google.protobuf.DurationB\x03\xe0\x41\x02R\x08\x64uration\x12M\n\x12\x65\x66\x66\x65\x63tive_duration\x18\x04 \x01(\x0b\x32\x19.google.protobuf.DurationB\x03\xe0\x41\x03R\x11\x65\x66\x66\x65\x63tiveDuration\x12>\n\nbegin_time\x18\x05 \x01(\x0b\x32\x1a.google.protobuf.TimestampH\x00R\tbeginTime\x88\x01\x01\x12V\n\x14\x65\x66\x66\x65\x63tive_begin_time\x18\x06 \x01(\x0b\x32\x1a.google.protobuf.TimestampB\x03\xe0\x41\x03H\x01R\x12\x65\x66\x66\x65\x63tiveBeginTime\x88\x01\x01\x12:\n\x08\x65nd_time\x18\x07 \x01(\x0b\x32\x1a.google.protobuf.TimestampH\x02R\x07\x65ndTime\x88\x01\x01\x12R\n\x12\x65\x66\x66\x65\x63tive_end_time\x18\x08 \x01(\x0b\x32\x1a.google.protobuf.TimestampB\x03\xe0\x41\x03H\x03R\x10\x65\x66\x66\x65\x63tiveEndTime\x88\x01\x01\x12;\n\x06\x63lient\x18\t \x01(\tB\x1e\xe0\x41\x03\xfa\x41\x18\n\x16jumpstarter.dev/ClientH\x04R\x06\x63lient\x88\x01\x01\x12\x41\n\x08\x65xporter\x18\n \x01(\tB \xe0\x41\x03\xfa\x41\x1a\n\x18jumpstarter.dev/ExporterH\x05R\x08\x65xporter\x88\x01\x01\x12>\n\nconditions\x18\x0b \x03(\x0b\x32\x19.jumpstarter.v1.ConditionB\x03\xe0\x41\x03R\nconditions:P\xea\x41M\n\x15jumpstarter.dev/Lease\x12%namespaces/{namespace}/leases/{lease}*\x06leases2\x05leaseB\r\n\x0b_begin_timeB\x17\n\x15_effective_begin_timeB\x0b\n\t_end_timeB\x15\n\x13_effective_end_timeB\t\n\x07_clientB\x0b\n\t_exporter\"J\n\x12GetExporterRequest\x12\x34\n\x04name\x18\x01 \x01(\tB \xe0\x41\x02\xfa\x41\x1a\n\x18jumpstarter.dev/ExporterR\x04name\"\xb3\x01\n\x14ListExportersRequest\x12\x38\n\x06parent\x18\x01 \x01(\tB \xe0\x41\x02\xfa\x41\x1a\x12\x18jumpstarter.dev/ExporterR\x06parent\x12 \n\tpage_size\x18\x02 \x01(\x05\x42\x03\xe0\x41\x01R\x08pageSize\x12\"\n\npage_token\x18\x03 \x01(\tB\x03\xe0\x41\x01R\tpageToken\x12\x1b\n\x06\x66ilter\x18\x04 \x01(\tB\x03\xe0\x41\x01R\x06\x66ilter\"~\n\x15ListExportersResponse\x12=\n\texporters\x18\x01 \x03(\x0b\x32\x1f.jumpstarter.client.v1.ExporterR\texporters\x12&\n\x0fnext_page_token\x18\x02 \x01(\tR\rnextPageToken\"D\n\x0fGetLeaseRequest\x12\x31\n\x04name\x18\x01 \x01(\tB\x1d\xe0\x41\x02\xfa\x41\x17\n\x15jumpstarter.dev/LeaseR\x04name\"\xad\x01\n\x11ListLeasesRequest\x12\x35\n\x06parent\x18\x01 \x01(\tB\x1d\xe0\x41\x02\xfa\x41\x17\x12\x15jumpstarter.dev/LeaseR\x06parent\x12 \n\tpage_size\x18\x02 \x01(\x05\x42\x03\xe0\x41\x01R\x08pageSize\x12\"\n\npage_token\x18\x03 \x01(\tB\x03\xe0\x41\x01R\tpageToken\x12\x1b\n\x06\x66ilter\x18\x04 \x01(\tB\x03\xe0\x41\x01R\x06\x66ilter\"r\n\x12ListLeasesResponse\x12\x34\n\x06leases\x18\x01 \x03(\x0b\x32\x1c.jumpstarter.client.v1.LeaseR\x06leases\x12&\n\x0fnext_page_token\x18\x02 \x01(\tR\rnextPageToken\"\xa4\x01\n\x12\x43reateLeaseRequest\x12\x35\n\x06parent\x18\x01 \x01(\tB\x1d\xe0\x41\x02\xfa\x41\x17\x12\x15jumpstarter.dev/LeaseR\x06parent\x12\x1e\n\x08lease_id\x18\x02 \x01(\tB\x03\xe0\x41\x01R\x07leaseId\x12\x37\n\x05lease\x18\x03 \x01(\x0b\x32\x1c.jumpstarter.client.v1.LeaseB\x03\xe0\x41\x02R\x05lease\"\x8f\x01\n\x12UpdateLeaseRequest\x12\x37\n\x05lease\x18\x01 \x01(\x0b\x32\x1c.jumpstarter.client.v1.LeaseB\x03\xe0\x41\x02R\x05lease\x12@\n\x0bupdate_mask\x18\x02 \x01(\x0b\x32\x1a.google.protobuf.FieldMaskB\x03\xe0\x41\x01R\nupdateMask\"G\n\x12\x44\x65leteLeaseRequest\x12\x31\n\x04name\x18\x01 \x01(\tB\x1d\xe0\x41\x02\xfa\x41\x17\n\x15jumpstarter.dev/LeaseR\x04name2\xa7\x08\n\rClientService\x12\x8d\x01\n\x0bGetExporter\x12).jumpstarter.client.v1.GetExporterRequest\x1a\x1f.jumpstarter.client.v1.Exporter\"2\xda\x41\x04name\x82\xd3\xe4\x93\x02%\x12#/v1/{name=namespaces/*/exporters/*}\x12\xa0\x01\n\rListExporters\x12+.jumpstarter.client.v1.ListExportersRequest\x1a,.jumpstarter.client.v1.ListExportersResponse\"4\xda\x41\x06parent\x82\xd3\xe4\x93\x02%\x12#/v1/{parent=namespaces/*}/exporters\x12\x81\x01\n\x08GetLease\x12&.jumpstarter.client.v1.GetLeaseRequest\x1a\x1c.jumpstarter.client.v1.Lease\"/\xda\x41\x04name\x82\xd3\xe4\x93\x02\"\x12 /v1/{name=namespaces/*/leases/*}\x12\x94\x01\n\nListLeases\x12(.jumpstarter.client.v1.ListLeasesRequest\x1a).jumpstarter.client.v1.ListLeasesResponse\"1\xda\x41\x06parent\x82\xd3\xe4\x93\x02\"\x12 /v1/{parent=namespaces/*}/leases\x12\x9f\x01\n\x0b\x43reateLease\x12).jumpstarter.client.v1.CreateLeaseRequest\x1a\x1c.jumpstarter.client.v1.Lease\"G\xda\x41\x15parent,lease,lease_id\x82\xd3\xe4\x93\x02)\" /v1/{parent=namespaces/*}/leases:\x05lease\x12\xa1\x01\n\x0bUpdateLease\x12).jumpstarter.client.v1.UpdateLeaseRequest\x1a\x1c.jumpstarter.client.v1.Lease\"I\xda\x41\x11lease,update_mask\x82\xd3\xe4\x93\x02/2&/v1/{lease.name=namespaces/*/leases/*}:\x05lease\x12\x81\x01\n\x0b\x44\x65leteLease\x12).jumpstarter.client.v1.DeleteLeaseRequest\x1a\x16.google.protobuf.Empty\"/\xda\x41\x04name\x82\xd3\xe4\x93\x02\"* /v1/{name=namespaces/*/leases/*}b\x06proto3')
_globals = globals()
_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals)
_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'jumpstarter.client.v1.client_pb2', _globals)
if not _descriptor._USE_C_DESCRIPTORS:
- _globals['DESCRIPTOR']._loaded_options = None
- _globals['DESCRIPTOR']._serialized_options = b'\n\031com.jumpstarter.client.v1B\013ClientProtoP\001\242\002\003JCX\252\002\025Jumpstarter.Client.V1\312\002\025Jumpstarter\\Client\\V1\342\002!Jumpstarter\\Client\\V1\\GPBMetadata\352\002\027Jumpstarter::Client::V1'
+ DESCRIPTOR._loaded_options = None
_globals['_EXPORTER_LABELSENTRY']._loaded_options = None
_globals['_EXPORTER_LABELSENTRY']._serialized_options = b'8\001'
_globals['_EXPORTER'].fields_by_name['name']._loaded_options = None
_globals['_EXPORTER'].fields_by_name['name']._serialized_options = b'\340A\010'
_globals['_EXPORTER'].fields_by_name['online']._loaded_options = None
- _globals['_EXPORTER'].fields_by_name['online']._serialized_options = b'\340A\003'
+ _globals['_EXPORTER'].fields_by_name['online']._serialized_options = b'\030\001\340A\003'
+ _globals['_EXPORTER'].fields_by_name['status']._loaded_options = None
+ _globals['_EXPORTER'].fields_by_name['status']._serialized_options = b'\340A\003'
_globals['_EXPORTER']._loaded_options = None
_globals['_EXPORTER']._serialized_options = b'\352A\\\n\030jumpstarter.dev/Exporter\022+namespaces/{namespace}/exporters/{exporter}*\texporters2\010exporter'
_globals['_LEASE'].fields_by_name['name']._loaded_options = None
@@ -115,30 +117,30 @@
_globals['_CLIENTSERVICE'].methods_by_name['UpdateLease']._serialized_options = b'\332A\021lease,update_mask\202\323\344\223\002/2&/v1/{lease.name=namespaces/*/leases/*}:\005lease'
_globals['_CLIENTSERVICE'].methods_by_name['DeleteLease']._loaded_options = None
_globals['_CLIENTSERVICE'].methods_by_name['DeleteLease']._serialized_options = b'\332A\004name\202\323\344\223\002\"* /v1/{name=namespaces/*/leases/*}'
- _globals['_EXPORTER']._serialized_start=338
- _globals['_EXPORTER']._serialized_end=627
- _globals['_EXPORTER_LABELSENTRY']._serialized_start=473
- _globals['_EXPORTER_LABELSENTRY']._serialized_end=530
- _globals['_LEASE']._serialized_start=630
- _globals['_LEASE']._serialized_end=1507
- _globals['_GETEXPORTERREQUEST']._serialized_start=1509
- _globals['_GETEXPORTERREQUEST']._serialized_end=1583
- _globals['_LISTEXPORTERSREQUEST']._serialized_start=1586
- _globals['_LISTEXPORTERSREQUEST']._serialized_end=1765
- _globals['_LISTEXPORTERSRESPONSE']._serialized_start=1767
- _globals['_LISTEXPORTERSRESPONSE']._serialized_end=1893
- _globals['_GETLEASEREQUEST']._serialized_start=1895
- _globals['_GETLEASEREQUEST']._serialized_end=1963
- _globals['_LISTLEASESREQUEST']._serialized_start=1966
- _globals['_LISTLEASESREQUEST']._serialized_end=2139
- _globals['_LISTLEASESRESPONSE']._serialized_start=2141
- _globals['_LISTLEASESRESPONSE']._serialized_end=2255
- _globals['_CREATELEASEREQUEST']._serialized_start=2258
- _globals['_CREATELEASEREQUEST']._serialized_end=2422
- _globals['_UPDATELEASEREQUEST']._serialized_start=2425
- _globals['_UPDATELEASEREQUEST']._serialized_end=2568
- _globals['_DELETELEASEREQUEST']._serialized_start=2570
- _globals['_DELETELEASEREQUEST']._serialized_end=2641
- _globals['_CLIENTSERVICE']._serialized_start=2644
- _globals['_CLIENTSERVICE']._serialized_end=3707
+ _globals['_EXPORTER']._serialized_start=367
+ _globals['_EXPORTER']._serialized_end=719
+ _globals['_EXPORTER_LABELSENTRY']._serialized_start=565
+ _globals['_EXPORTER_LABELSENTRY']._serialized_end=622
+ _globals['_LEASE']._serialized_start=722
+ _globals['_LEASE']._serialized_end=1599
+ _globals['_GETEXPORTERREQUEST']._serialized_start=1601
+ _globals['_GETEXPORTERREQUEST']._serialized_end=1675
+ _globals['_LISTEXPORTERSREQUEST']._serialized_start=1678
+ _globals['_LISTEXPORTERSREQUEST']._serialized_end=1857
+ _globals['_LISTEXPORTERSRESPONSE']._serialized_start=1859
+ _globals['_LISTEXPORTERSRESPONSE']._serialized_end=1985
+ _globals['_GETLEASEREQUEST']._serialized_start=1987
+ _globals['_GETLEASEREQUEST']._serialized_end=2055
+ _globals['_LISTLEASESREQUEST']._serialized_start=2058
+ _globals['_LISTLEASESREQUEST']._serialized_end=2231
+ _globals['_LISTLEASESRESPONSE']._serialized_start=2233
+ _globals['_LISTLEASESRESPONSE']._serialized_end=2347
+ _globals['_CREATELEASEREQUEST']._serialized_start=2350
+ _globals['_CREATELEASEREQUEST']._serialized_end=2514
+ _globals['_UPDATELEASEREQUEST']._serialized_start=2517
+ _globals['_UPDATELEASEREQUEST']._serialized_end=2660
+ _globals['_DELETELEASEREQUEST']._serialized_start=2662
+ _globals['_DELETELEASEREQUEST']._serialized_end=2733
+ _globals['_CLIENTSERVICE']._serialized_start=2736
+ _globals['_CLIENTSERVICE']._serialized_end=3799
# @@protoc_insertion_point(module_scope)
diff --git a/packages/jumpstarter-protocol/jumpstarter_protocol/jumpstarter/v1/common_pb2.py b/packages/jumpstarter-protocol/jumpstarter_protocol/jumpstarter/v1/common_pb2.py
new file mode 100644
index 000000000..6349b917c
--- /dev/null
+++ b/packages/jumpstarter-protocol/jumpstarter_protocol/jumpstarter/v1/common_pb2.py
@@ -0,0 +1,38 @@
+# -*- coding: utf-8 -*-
+# Generated by the protocol buffer compiler. DO NOT EDIT!
+# NO CHECKED-IN PROTOBUF GENCODE
+# source: jumpstarter/v1/common.proto
+# Protobuf Python Version: 6.30.1
+"""Generated protocol buffer code."""
+from google.protobuf import descriptor as _descriptor
+from google.protobuf import descriptor_pool as _descriptor_pool
+from google.protobuf import runtime_version as _runtime_version
+from google.protobuf import symbol_database as _symbol_database
+from google.protobuf.internal import builder as _builder
+_runtime_version.ValidateProtobufRuntimeVersion(
+ _runtime_version.Domain.PUBLIC,
+ 6,
+ 30,
+ 1,
+ '',
+ 'jumpstarter/v1/common.proto'
+)
+# @@protoc_insertion_point(imports)
+
+_sym_db = _symbol_database.Default()
+
+
+
+
+DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x1bjumpstarter/v1/common.proto\x12\x0ejumpstarter.v1*\xb6\x02\n\x0e\x45xporterStatus\x12\x1f\n\x1b\x45XPORTER_STATUS_UNSPECIFIED\x10\x00\x12\x1b\n\x17\x45XPORTER_STATUS_OFFLINE\x10\x01\x12\x1d\n\x19\x45XPORTER_STATUS_AVAILABLE\x10\x02\x12%\n!EXPORTER_STATUS_BEFORE_LEASE_HOOK\x10\x03\x12\x1f\n\x1b\x45XPORTER_STATUS_LEASE_READY\x10\x04\x12$\n EXPORTER_STATUS_AFTER_LEASE_HOOK\x10\x05\x12,\n(EXPORTER_STATUS_BEFORE_LEASE_HOOK_FAILED\x10\x06\x12+\n\'EXPORTER_STATUS_AFTER_LEASE_HOOK_FAILED\x10\x07*\x98\x01\n\tLogSource\x12\x1a\n\x16LOG_SOURCE_UNSPECIFIED\x10\x00\x12\x15\n\x11LOG_SOURCE_DRIVER\x10\x01\x12 \n\x1cLOG_SOURCE_BEFORE_LEASE_HOOK\x10\x02\x12\x1f\n\x1bLOG_SOURCE_AFTER_LEASE_HOOK\x10\x03\x12\x15\n\x11LOG_SOURCE_SYSTEM\x10\x04\x62\x06proto3')
+
+_globals = globals()
+_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals)
+_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'jumpstarter.v1.common_pb2', _globals)
+if not _descriptor._USE_C_DESCRIPTORS:
+ DESCRIPTOR._loaded_options = None
+ _globals['_EXPORTERSTATUS']._serialized_start=48
+ _globals['_EXPORTERSTATUS']._serialized_end=358
+ _globals['_LOGSOURCE']._serialized_start=361
+ _globals['_LOGSOURCE']._serialized_end=513
+# @@protoc_insertion_point(module_scope)
diff --git a/packages/jumpstarter-protocol/jumpstarter_protocol/jumpstarter/v1/common_pb2_grpc.py b/packages/jumpstarter-protocol/jumpstarter_protocol/jumpstarter/v1/common_pb2_grpc.py
new file mode 100644
index 000000000..2daafffeb
--- /dev/null
+++ b/packages/jumpstarter-protocol/jumpstarter_protocol/jumpstarter/v1/common_pb2_grpc.py
@@ -0,0 +1,4 @@
+# Generated by the gRPC Python protocol compiler plugin. DO NOT EDIT!
+"""Client and server classes corresponding to protobuf-defined services."""
+import grpc
+
diff --git a/packages/jumpstarter-protocol/jumpstarter_protocol/jumpstarter/v1/jumpstarter_pb2.py b/packages/jumpstarter-protocol/jumpstarter_protocol/jumpstarter/v1/jumpstarter_pb2.py
index d75ddd9d4..10892f748 100644
--- a/packages/jumpstarter-protocol/jumpstarter_protocol/jumpstarter/v1/jumpstarter_pb2.py
+++ b/packages/jumpstarter-protocol/jumpstarter_protocol/jumpstarter/v1/jumpstarter_pb2.py
@@ -27,88 +27,100 @@
from google.protobuf import struct_pb2 as google_dot_protobuf_dot_struct__pb2
from google.protobuf import timestamp_pb2 as google_dot_protobuf_dot_timestamp__pb2
from . import kubernetes_pb2 as jumpstarter_dot_v1_dot_kubernetes__pb2
+from . import common_pb2 as jumpstarter_dot_v1_dot_common__pb2
-DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n jumpstarter/v1/jumpstarter.proto\x12\x0ejumpstarter.v1\x1a\x1egoogle/protobuf/duration.proto\x1a\x1bgoogle/protobuf/empty.proto\x1a\x1cgoogle/protobuf/struct.proto\x1a\x1fgoogle/protobuf/timestamp.proto\x1a\x1fjumpstarter/v1/kubernetes.proto\"\xd1\x01\n\x0fRegisterRequest\x12\x43\n\x06labels\x18\x01 \x03(\x0b\x32+.jumpstarter.v1.RegisterRequest.LabelsEntryR\x06labels\x12>\n\x07reports\x18\x02 \x03(\x0b\x32$.jumpstarter.v1.DriverInstanceReportR\x07reports\x1a\x39\n\x0bLabelsEntry\x12\x10\n\x03key\x18\x01 \x01(\tR\x03key\x12\x14\n\x05value\x18\x02 \x01(\tR\x05value:\x02\x38\x01\"\xe5\x01\n\x14\x44riverInstanceReport\x12\x12\n\x04uuid\x18\x01 \x01(\tR\x04uuid\x12$\n\x0bparent_uuid\x18\x02 \x01(\tH\x00R\nparentUuid\x88\x01\x01\x12H\n\x06labels\x18\x03 \x03(\x0b\x32\x30.jumpstarter.v1.DriverInstanceReport.LabelsEntryR\x06labels\x1a\x39\n\x0bLabelsEntry\x12\x10\n\x03key\x18\x01 \x01(\tR\x03key\x12\x14\n\x05value\x18\x02 \x01(\tR\x05value:\x02\x38\x01\x42\x0e\n\x0c_parent_uuid\"&\n\x10RegisterResponse\x12\x12\n\x04uuid\x18\x01 \x01(\tR\x04uuid\"+\n\x11UnregisterRequest\x12\x16\n\x06reason\x18\x02 \x01(\tR\x06reason\"\x14\n\x12UnregisterResponse\".\n\rListenRequest\x12\x1d\n\nlease_name\x18\x01 \x01(\tR\tleaseName\"\\\n\x0eListenResponse\x12\'\n\x0frouter_endpoint\x18\x01 \x01(\tR\x0erouterEndpoint\x12!\n\x0crouter_token\x18\x02 \x01(\tR\x0brouterToken\"\x0f\n\rStatusRequest\"\x91\x01\n\x0eStatusResponse\x12\x16\n\x06leased\x18\x01 \x01(\x08R\x06leased\x12\"\n\nlease_name\x18\x02 \x01(\tH\x00R\tleaseName\x88\x01\x01\x12$\n\x0b\x63lient_name\x18\x03 \x01(\tH\x01R\nclientName\x88\x01\x01\x42\r\n\x0b_lease_nameB\x0e\n\x0c_client_name\",\n\x0b\x44ialRequest\x12\x1d\n\nlease_name\x18\x01 \x01(\tR\tleaseName\"Z\n\x0c\x44ialResponse\x12\'\n\x0frouter_endpoint\x18\x01 \x01(\tR\x0erouterEndpoint\x12!\n\x0crouter_token\x18\x02 \x01(\tR\x0brouterToken\"\xa1\x01\n\x12\x41uditStreamRequest\x12#\n\rexporter_uuid\x18\x01 \x01(\tR\x0c\x65xporterUuid\x12\x30\n\x14\x64river_instance_uuid\x18\x02 \x01(\tR\x12\x64riverInstanceUuid\x12\x1a\n\x08severity\x18\x03 \x01(\tR\x08severity\x12\x18\n\x07message\x18\x04 \x01(\tR\x07message\"\xb8\x02\n\x11GetReportResponse\x12\x12\n\x04uuid\x18\x01 \x01(\tR\x04uuid\x12\x45\n\x06labels\x18\x02 \x03(\x0b\x32-.jumpstarter.v1.GetReportResponse.LabelsEntryR\x06labels\x12>\n\x07reports\x18\x03 \x03(\x0b\x32$.jumpstarter.v1.DriverInstanceReportR\x07reports\x12M\n\x15\x61lternative_endpoints\x18\x04 \x03(\x0b\x32\x18.jumpstarter.v1.EndpointR\x14\x61lternativeEndpoints\x1a\x39\n\x0bLabelsEntry\x12\x10\n\x03key\x18\x01 \x01(\tR\x03key\x12\x14\n\x05value\x18\x02 \x01(\tR\x05value:\x02\x38\x01\"\xa5\x01\n\x08\x45ndpoint\x12\x1a\n\x08\x65ndpoint\x18\x01 \x01(\tR\x08\x65ndpoint\x12 \n\x0b\x63\x65rtificate\x18\x02 \x01(\tR\x0b\x63\x65rtificate\x12-\n\x12\x63lient_certificate\x18\x03 \x01(\tR\x11\x63lientCertificate\x12,\n\x12\x63lient_private_key\x18\x04 \x01(\tR\x10\x63lientPrivateKey\"k\n\x11\x44riverCallRequest\x12\x12\n\x04uuid\x18\x01 \x01(\tR\x04uuid\x12\x16\n\x06method\x18\x02 \x01(\tR\x06method\x12*\n\x04\x61rgs\x18\x03 \x03(\x0b\x32\x16.google.protobuf.ValueR\x04\x61rgs\"X\n\x12\x44riverCallResponse\x12\x12\n\x04uuid\x18\x01 \x01(\tR\x04uuid\x12.\n\x06result\x18\x02 \x01(\x0b\x32\x16.google.protobuf.ValueR\x06result\"t\n\x1aStreamingDriverCallRequest\x12\x12\n\x04uuid\x18\x01 \x01(\tR\x04uuid\x12\x16\n\x06method\x18\x02 \x01(\tR\x06method\x12*\n\x04\x61rgs\x18\x03 \x03(\x0b\x32\x16.google.protobuf.ValueR\x04\x61rgs\"a\n\x1bStreamingDriverCallResponse\x12\x12\n\x04uuid\x18\x01 \x01(\tR\x04uuid\x12.\n\x06result\x18\x02 \x01(\x0b\x32\x16.google.protobuf.ValueR\x06result\"]\n\x11LogStreamResponse\x12\x12\n\x04uuid\x18\x01 \x01(\tR\x04uuid\x12\x1a\n\x08severity\x18\x02 \x01(\tR\x08severity\x12\x18\n\x07message\x18\x03 \x01(\tR\x07message\"\x0e\n\x0cResetRequest\"\x0f\n\rResetResponse\"%\n\x0fGetLeaseRequest\x12\x12\n\x04name\x18\x01 \x01(\tR\x04name\"\x93\x03\n\x10GetLeaseResponse\x12\x35\n\x08\x64uration\x18\x01 \x01(\x0b\x32\x19.google.protobuf.DurationR\x08\x64uration\x12\x39\n\x08selector\x18\x02 \x01(\x0b\x32\x1d.jumpstarter.v1.LabelSelectorR\x08selector\x12>\n\nbegin_time\x18\x03 \x01(\x0b\x32\x1a.google.protobuf.TimestampH\x00R\tbeginTime\x88\x01\x01\x12:\n\x08\x65nd_time\x18\x04 \x01(\x0b\x32\x1a.google.protobuf.TimestampH\x01R\x07\x65ndTime\x88\x01\x01\x12(\n\rexporter_uuid\x18\x05 \x01(\tH\x02R\x0c\x65xporterUuid\x88\x01\x01\x12\x39\n\nconditions\x18\x06 \x03(\x0b\x32\x19.jumpstarter.v1.ConditionR\nconditionsB\r\n\x0b_begin_timeB\x0b\n\t_end_timeB\x10\n\x0e_exporter_uuid\"\x87\x01\n\x13RequestLeaseRequest\x12\x35\n\x08\x64uration\x18\x01 \x01(\x0b\x32\x19.google.protobuf.DurationR\x08\x64uration\x12\x39\n\x08selector\x18\x02 \x01(\x0b\x32\x1d.jumpstarter.v1.LabelSelectorR\x08selector\"*\n\x14RequestLeaseResponse\x12\x12\n\x04name\x18\x01 \x01(\tR\x04name\")\n\x13ReleaseLeaseRequest\x12\x12\n\x04name\x18\x01 \x01(\tR\x04name\"\x16\n\x14ReleaseLeaseResponse\"\x13\n\x11ListLeasesRequest\"*\n\x12ListLeasesResponse\x12\x14\n\x05names\x18\x01 \x03(\tR\x05names2\xb7\x06\n\x11\x43ontrollerService\x12M\n\x08Register\x12\x1f.jumpstarter.v1.RegisterRequest\x1a .jumpstarter.v1.RegisterResponse\x12S\n\nUnregister\x12!.jumpstarter.v1.UnregisterRequest\x1a\".jumpstarter.v1.UnregisterResponse\x12I\n\x06Listen\x12\x1d.jumpstarter.v1.ListenRequest\x1a\x1e.jumpstarter.v1.ListenResponse0\x01\x12I\n\x06Status\x12\x1d.jumpstarter.v1.StatusRequest\x1a\x1e.jumpstarter.v1.StatusResponse0\x01\x12\x41\n\x04\x44ial\x12\x1b.jumpstarter.v1.DialRequest\x1a\x1c.jumpstarter.v1.DialResponse\x12K\n\x0b\x41uditStream\x12\".jumpstarter.v1.AuditStreamRequest\x1a\x16.google.protobuf.Empty(\x01\x12M\n\x08GetLease\x12\x1f.jumpstarter.v1.GetLeaseRequest\x1a .jumpstarter.v1.GetLeaseResponse\x12Y\n\x0cRequestLease\x12#.jumpstarter.v1.RequestLeaseRequest\x1a$.jumpstarter.v1.RequestLeaseResponse\x12Y\n\x0cReleaseLease\x12#.jumpstarter.v1.ReleaseLeaseRequest\x1a$.jumpstarter.v1.ReleaseLeaseResponse\x12S\n\nListLeases\x12!.jumpstarter.v1.ListLeasesRequest\x1a\".jumpstarter.v1.ListLeasesResponse2\xb0\x03\n\x0f\x45xporterService\x12\x46\n\tGetReport\x12\x16.google.protobuf.Empty\x1a!.jumpstarter.v1.GetReportResponse\x12S\n\nDriverCall\x12!.jumpstarter.v1.DriverCallRequest\x1a\".jumpstarter.v1.DriverCallResponse\x12p\n\x13StreamingDriverCall\x12*.jumpstarter.v1.StreamingDriverCallRequest\x1a+.jumpstarter.v1.StreamingDriverCallResponse0\x01\x12H\n\tLogStream\x12\x16.google.protobuf.Empty\x1a!.jumpstarter.v1.LogStreamResponse0\x01\x12\x44\n\x05Reset\x12\x1c.jumpstarter.v1.ResetRequest\x1a\x1d.jumpstarter.v1.ResetResponseB\x7f\n\x12\x63om.jumpstarter.v1B\x10JumpstarterProtoP\x01\xa2\x02\x03JXX\xaa\x02\x0eJumpstarter.V1\xca\x02\x0eJumpstarter\\V1\xe2\x02\x1aJumpstarter\\V1\\GPBMetadata\xea\x02\x0fJumpstarter::V1b\x06proto3')
+DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n jumpstarter/v1/jumpstarter.proto\x12\x0ejumpstarter.v1\x1a\x1egoogle/protobuf/duration.proto\x1a\x1bgoogle/protobuf/empty.proto\x1a\x1cgoogle/protobuf/struct.proto\x1a\x1fgoogle/protobuf/timestamp.proto\x1a\x1fjumpstarter/v1/kubernetes.proto\x1a\x1bjumpstarter/v1/common.proto\"\xd1\x01\n\x0fRegisterRequest\x12\x43\n\x06labels\x18\x01 \x03(\x0b\x32+.jumpstarter.v1.RegisterRequest.LabelsEntryR\x06labels\x12>\n\x07reports\x18\x02 \x03(\x0b\x32$.jumpstarter.v1.DriverInstanceReportR\x07reports\x1a\x39\n\x0bLabelsEntry\x12\x10\n\x03key\x18\x01 \x01(\tR\x03key\x12\x14\n\x05value\x18\x02 \x01(\tR\x05value:\x02\x38\x01\"\xd2\x03\n\x14\x44riverInstanceReport\x12\x12\n\x04uuid\x18\x01 \x01(\tR\x04uuid\x12$\n\x0bparent_uuid\x18\x02 \x01(\tH\x00R\nparentUuid\x88\x01\x01\x12H\n\x06labels\x18\x03 \x03(\x0b\x32\x30.jumpstarter.v1.DriverInstanceReport.LabelsEntryR\x06labels\x12%\n\x0b\x64\x65scription\x18\x04 \x01(\tH\x01R\x0b\x64\x65scription\x88\x01\x01\x12m\n\x13methods_description\x18\x05 \x03(\x0b\x32<.jumpstarter.v1.DriverInstanceReport.MethodsDescriptionEntryR\x12methodsDescription\x1a\x39\n\x0bLabelsEntry\x12\x10\n\x03key\x18\x01 \x01(\tR\x03key\x12\x14\n\x05value\x18\x02 \x01(\tR\x05value:\x02\x38\x01\x1a\x45\n\x17MethodsDescriptionEntry\x12\x10\n\x03key\x18\x01 \x01(\tR\x03key\x12\x14\n\x05value\x18\x02 \x01(\tR\x05value:\x02\x38\x01\x42\x0e\n\x0c_parent_uuidB\x0e\n\x0c_description\"&\n\x10RegisterResponse\x12\x12\n\x04uuid\x18\x01 \x01(\tR\x04uuid\"+\n\x11UnregisterRequest\x12\x16\n\x06reason\x18\x02 \x01(\tR\x06reason\"\x14\n\x12UnregisterResponse\".\n\rListenRequest\x12\x1d\n\nlease_name\x18\x01 \x01(\tR\tleaseName\"\\\n\x0eListenResponse\x12\'\n\x0frouter_endpoint\x18\x01 \x01(\tR\x0erouterEndpoint\x12!\n\x0crouter_token\x18\x02 \x01(\tR\x0brouterToken\"\x0f\n\rStatusRequest\"\x91\x01\n\x0eStatusResponse\x12\x16\n\x06leased\x18\x01 \x01(\x08R\x06leased\x12\"\n\nlease_name\x18\x02 \x01(\tH\x00R\tleaseName\x88\x01\x01\x12$\n\x0b\x63lient_name\x18\x03 \x01(\tH\x01R\nclientName\x88\x01\x01\x42\r\n\x0b_lease_nameB\x0e\n\x0c_client_name\",\n\x0b\x44ialRequest\x12\x1d\n\nlease_name\x18\x01 \x01(\tR\tleaseName\"Z\n\x0c\x44ialResponse\x12\'\n\x0frouter_endpoint\x18\x01 \x01(\tR\x0erouterEndpoint\x12!\n\x0crouter_token\x18\x02 \x01(\tR\x0brouterToken\"\xa1\x01\n\x12\x41uditStreamRequest\x12#\n\rexporter_uuid\x18\x01 \x01(\tR\x0c\x65xporterUuid\x12\x30\n\x14\x64river_instance_uuid\x18\x02 \x01(\tR\x12\x64riverInstanceUuid\x12\x1a\n\x08severity\x18\x03 \x01(\tR\x08severity\x12\x18\n\x07message\x18\x04 \x01(\tR\x07message\"x\n\x13ReportStatusRequest\x12\x36\n\x06status\x18\x01 \x01(\x0e\x32\x1e.jumpstarter.v1.ExporterStatusR\x06status\x12\x1d\n\x07message\x18\x02 \x01(\tH\x00R\x07message\x88\x01\x01\x42\n\n\x08_message\"\x16\n\x14ReportStatusResponse\"\xb8\x02\n\x11GetReportResponse\x12\x12\n\x04uuid\x18\x01 \x01(\tR\x04uuid\x12\x45\n\x06labels\x18\x02 \x03(\x0b\x32-.jumpstarter.v1.GetReportResponse.LabelsEntryR\x06labels\x12>\n\x07reports\x18\x03 \x03(\x0b\x32$.jumpstarter.v1.DriverInstanceReportR\x07reports\x12M\n\x15\x61lternative_endpoints\x18\x04 \x03(\x0b\x32\x18.jumpstarter.v1.EndpointR\x14\x61lternativeEndpoints\x1a\x39\n\x0bLabelsEntry\x12\x10\n\x03key\x18\x01 \x01(\tR\x03key\x12\x14\n\x05value\x18\x02 \x01(\tR\x05value:\x02\x38\x01\"\xa5\x01\n\x08\x45ndpoint\x12\x1a\n\x08\x65ndpoint\x18\x01 \x01(\tR\x08\x65ndpoint\x12 \n\x0b\x63\x65rtificate\x18\x02 \x01(\tR\x0b\x63\x65rtificate\x12-\n\x12\x63lient_certificate\x18\x03 \x01(\tR\x11\x63lientCertificate\x12,\n\x12\x63lient_private_key\x18\x04 \x01(\tR\x10\x63lientPrivateKey\"k\n\x11\x44riverCallRequest\x12\x12\n\x04uuid\x18\x01 \x01(\tR\x04uuid\x12\x16\n\x06method\x18\x02 \x01(\tR\x06method\x12*\n\x04\x61rgs\x18\x03 \x03(\x0b\x32\x16.google.protobuf.ValueR\x04\x61rgs\"X\n\x12\x44riverCallResponse\x12\x12\n\x04uuid\x18\x01 \x01(\tR\x04uuid\x12.\n\x06result\x18\x02 \x01(\x0b\x32\x16.google.protobuf.ValueR\x06result\"t\n\x1aStreamingDriverCallRequest\x12\x12\n\x04uuid\x18\x01 \x01(\tR\x04uuid\x12\x16\n\x06method\x18\x02 \x01(\tR\x06method\x12*\n\x04\x61rgs\x18\x03 \x03(\x0b\x32\x16.google.protobuf.ValueR\x04\x61rgs\"a\n\x1bStreamingDriverCallResponse\x12\x12\n\x04uuid\x18\x01 \x01(\tR\x04uuid\x12.\n\x06result\x18\x02 \x01(\x0b\x32\x16.google.protobuf.ValueR\x06result\"\xa0\x01\n\x11LogStreamResponse\x12\x12\n\x04uuid\x18\x01 \x01(\tR\x04uuid\x12\x1a\n\x08severity\x18\x02 \x01(\tR\x08severity\x12\x18\n\x07message\x18\x03 \x01(\tR\x07message\x12\x36\n\x06source\x18\x04 \x01(\x0e\x32\x19.jumpstarter.v1.LogSourceH\x00R\x06source\x88\x01\x01\x42\t\n\x07_source\"\x0e\n\x0cResetRequest\"\x0f\n\rResetResponse\"%\n\x0fGetLeaseRequest\x12\x12\n\x04name\x18\x01 \x01(\tR\x04name\"\x93\x03\n\x10GetLeaseResponse\x12\x35\n\x08\x64uration\x18\x01 \x01(\x0b\x32\x19.google.protobuf.DurationR\x08\x64uration\x12\x39\n\x08selector\x18\x02 \x01(\x0b\x32\x1d.jumpstarter.v1.LabelSelectorR\x08selector\x12>\n\nbegin_time\x18\x03 \x01(\x0b\x32\x1a.google.protobuf.TimestampH\x00R\tbeginTime\x88\x01\x01\x12:\n\x08\x65nd_time\x18\x04 \x01(\x0b\x32\x1a.google.protobuf.TimestampH\x01R\x07\x65ndTime\x88\x01\x01\x12(\n\rexporter_uuid\x18\x05 \x01(\tH\x02R\x0c\x65xporterUuid\x88\x01\x01\x12\x39\n\nconditions\x18\x06 \x03(\x0b\x32\x19.jumpstarter.v1.ConditionR\nconditionsB\r\n\x0b_begin_timeB\x0b\n\t_end_timeB\x10\n\x0e_exporter_uuid\"\x87\x01\n\x13RequestLeaseRequest\x12\x35\n\x08\x64uration\x18\x01 \x01(\x0b\x32\x19.google.protobuf.DurationR\x08\x64uration\x12\x39\n\x08selector\x18\x02 \x01(\x0b\x32\x1d.jumpstarter.v1.LabelSelectorR\x08selector\"*\n\x14RequestLeaseResponse\x12\x12\n\x04name\x18\x01 \x01(\tR\x04name\")\n\x13ReleaseLeaseRequest\x12\x12\n\x04name\x18\x01 \x01(\tR\x04name\"\x16\n\x14ReleaseLeaseResponse\"\x13\n\x11ListLeasesRequest\"*\n\x12ListLeasesResponse\x12\x14\n\x05names\x18\x01 \x03(\tR\x05names\"\x12\n\x10GetStatusRequest\"v\n\x11GetStatusResponse\x12\x36\n\x06status\x18\x01 \x01(\x0e\x32\x1e.jumpstarter.v1.ExporterStatusR\x06status\x12\x1d\n\x07message\x18\x02 \x01(\tH\x00R\x07message\x88\x01\x01\x42\n\n\x08_message2\x92\x07\n\x11\x43ontrollerService\x12M\n\x08Register\x12\x1f.jumpstarter.v1.RegisterRequest\x1a .jumpstarter.v1.RegisterResponse\x12S\n\nUnregister\x12!.jumpstarter.v1.UnregisterRequest\x1a\".jumpstarter.v1.UnregisterResponse\x12Y\n\x0cReportStatus\x12#.jumpstarter.v1.ReportStatusRequest\x1a$.jumpstarter.v1.ReportStatusResponse\x12I\n\x06Listen\x12\x1d.jumpstarter.v1.ListenRequest\x1a\x1e.jumpstarter.v1.ListenResponse0\x01\x12I\n\x06Status\x12\x1d.jumpstarter.v1.StatusRequest\x1a\x1e.jumpstarter.v1.StatusResponse0\x01\x12\x41\n\x04\x44ial\x12\x1b.jumpstarter.v1.DialRequest\x1a\x1c.jumpstarter.v1.DialResponse\x12K\n\x0b\x41uditStream\x12\".jumpstarter.v1.AuditStreamRequest\x1a\x16.google.protobuf.Empty(\x01\x12M\n\x08GetLease\x12\x1f.jumpstarter.v1.GetLeaseRequest\x1a .jumpstarter.v1.GetLeaseResponse\x12Y\n\x0cRequestLease\x12#.jumpstarter.v1.RequestLeaseRequest\x1a$.jumpstarter.v1.RequestLeaseResponse\x12Y\n\x0cReleaseLease\x12#.jumpstarter.v1.ReleaseLeaseRequest\x1a$.jumpstarter.v1.ReleaseLeaseResponse\x12S\n\nListLeases\x12!.jumpstarter.v1.ListLeasesRequest\x1a\".jumpstarter.v1.ListLeasesResponse2\x82\x04\n\x0f\x45xporterService\x12\x46\n\tGetReport\x12\x16.google.protobuf.Empty\x1a!.jumpstarter.v1.GetReportResponse\x12S\n\nDriverCall\x12!.jumpstarter.v1.DriverCallRequest\x1a\".jumpstarter.v1.DriverCallResponse\x12p\n\x13StreamingDriverCall\x12*.jumpstarter.v1.StreamingDriverCallRequest\x1a+.jumpstarter.v1.StreamingDriverCallResponse0\x01\x12H\n\tLogStream\x12\x16.google.protobuf.Empty\x1a!.jumpstarter.v1.LogStreamResponse0\x01\x12\x44\n\x05Reset\x12\x1c.jumpstarter.v1.ResetRequest\x1a\x1d.jumpstarter.v1.ResetResponse\x12P\n\tGetStatus\x12 .jumpstarter.v1.GetStatusRequest\x1a!.jumpstarter.v1.GetStatusResponseb\x06proto3')
_globals = globals()
_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals)
_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'jumpstarter.v1.jumpstarter_pb2', _globals)
if not _descriptor._USE_C_DESCRIPTORS:
- _globals['DESCRIPTOR']._loaded_options = None
- _globals['DESCRIPTOR']._serialized_options = b'\n\022com.jumpstarter.v1B\020JumpstarterProtoP\001\242\002\003JXX\252\002\016Jumpstarter.V1\312\002\016Jumpstarter\\V1\342\002\032Jumpstarter\\V1\\GPBMetadata\352\002\017Jumpstarter::V1'
+ DESCRIPTOR._loaded_options = None
_globals['_REGISTERREQUEST_LABELSENTRY']._loaded_options = None
_globals['_REGISTERREQUEST_LABELSENTRY']._serialized_options = b'8\001'
_globals['_DRIVERINSTANCEREPORT_LABELSENTRY']._loaded_options = None
_globals['_DRIVERINSTANCEREPORT_LABELSENTRY']._serialized_options = b'8\001'
+ _globals['_DRIVERINSTANCEREPORT_METHODSDESCRIPTIONENTRY']._loaded_options = None
+ _globals['_DRIVERINSTANCEREPORT_METHODSDESCRIPTIONENTRY']._serialized_options = b'8\001'
_globals['_GETREPORTRESPONSE_LABELSENTRY']._loaded_options = None
_globals['_GETREPORTRESPONSE_LABELSENTRY']._serialized_options = b'8\001'
- _globals['_REGISTERREQUEST']._serialized_start=210
- _globals['_REGISTERREQUEST']._serialized_end=419
- _globals['_REGISTERREQUEST_LABELSENTRY']._serialized_start=362
- _globals['_REGISTERREQUEST_LABELSENTRY']._serialized_end=419
- _globals['_DRIVERINSTANCEREPORT']._serialized_start=422
- _globals['_DRIVERINSTANCEREPORT']._serialized_end=651
- _globals['_DRIVERINSTANCEREPORT_LABELSENTRY']._serialized_start=362
- _globals['_DRIVERINSTANCEREPORT_LABELSENTRY']._serialized_end=419
- _globals['_REGISTERRESPONSE']._serialized_start=653
- _globals['_REGISTERRESPONSE']._serialized_end=691
- _globals['_UNREGISTERREQUEST']._serialized_start=693
- _globals['_UNREGISTERREQUEST']._serialized_end=736
- _globals['_UNREGISTERRESPONSE']._serialized_start=738
- _globals['_UNREGISTERRESPONSE']._serialized_end=758
- _globals['_LISTENREQUEST']._serialized_start=760
- _globals['_LISTENREQUEST']._serialized_end=806
- _globals['_LISTENRESPONSE']._serialized_start=808
- _globals['_LISTENRESPONSE']._serialized_end=900
- _globals['_STATUSREQUEST']._serialized_start=902
- _globals['_STATUSREQUEST']._serialized_end=917
- _globals['_STATUSRESPONSE']._serialized_start=920
- _globals['_STATUSRESPONSE']._serialized_end=1065
- _globals['_DIALREQUEST']._serialized_start=1067
- _globals['_DIALREQUEST']._serialized_end=1111
- _globals['_DIALRESPONSE']._serialized_start=1113
- _globals['_DIALRESPONSE']._serialized_end=1203
- _globals['_AUDITSTREAMREQUEST']._serialized_start=1206
- _globals['_AUDITSTREAMREQUEST']._serialized_end=1367
- _globals['_GETREPORTRESPONSE']._serialized_start=1370
- _globals['_GETREPORTRESPONSE']._serialized_end=1682
- _globals['_GETREPORTRESPONSE_LABELSENTRY']._serialized_start=362
- _globals['_GETREPORTRESPONSE_LABELSENTRY']._serialized_end=419
- _globals['_ENDPOINT']._serialized_start=1685
- _globals['_ENDPOINT']._serialized_end=1850
- _globals['_DRIVERCALLREQUEST']._serialized_start=1852
- _globals['_DRIVERCALLREQUEST']._serialized_end=1959
- _globals['_DRIVERCALLRESPONSE']._serialized_start=1961
- _globals['_DRIVERCALLRESPONSE']._serialized_end=2049
- _globals['_STREAMINGDRIVERCALLREQUEST']._serialized_start=2051
- _globals['_STREAMINGDRIVERCALLREQUEST']._serialized_end=2167
- _globals['_STREAMINGDRIVERCALLRESPONSE']._serialized_start=2169
- _globals['_STREAMINGDRIVERCALLRESPONSE']._serialized_end=2266
- _globals['_LOGSTREAMRESPONSE']._serialized_start=2268
- _globals['_LOGSTREAMRESPONSE']._serialized_end=2361
- _globals['_RESETREQUEST']._serialized_start=2363
- _globals['_RESETREQUEST']._serialized_end=2377
- _globals['_RESETRESPONSE']._serialized_start=2379
- _globals['_RESETRESPONSE']._serialized_end=2394
- _globals['_GETLEASEREQUEST']._serialized_start=2396
- _globals['_GETLEASEREQUEST']._serialized_end=2433
- _globals['_GETLEASERESPONSE']._serialized_start=2436
- _globals['_GETLEASERESPONSE']._serialized_end=2839
- _globals['_REQUESTLEASEREQUEST']._serialized_start=2842
- _globals['_REQUESTLEASEREQUEST']._serialized_end=2977
- _globals['_REQUESTLEASERESPONSE']._serialized_start=2979
- _globals['_REQUESTLEASERESPONSE']._serialized_end=3021
- _globals['_RELEASELEASEREQUEST']._serialized_start=3023
- _globals['_RELEASELEASEREQUEST']._serialized_end=3064
- _globals['_RELEASELEASERESPONSE']._serialized_start=3066
- _globals['_RELEASELEASERESPONSE']._serialized_end=3088
- _globals['_LISTLEASESREQUEST']._serialized_start=3090
- _globals['_LISTLEASESREQUEST']._serialized_end=3109
- _globals['_LISTLEASESRESPONSE']._serialized_start=3111
- _globals['_LISTLEASESRESPONSE']._serialized_end=3153
- _globals['_CONTROLLERSERVICE']._serialized_start=3156
- _globals['_CONTROLLERSERVICE']._serialized_end=3979
- _globals['_EXPORTERSERVICE']._serialized_start=3982
- _globals['_EXPORTERSERVICE']._serialized_end=4414
+ _globals['_REGISTERREQUEST']._serialized_start=239
+ _globals['_REGISTERREQUEST']._serialized_end=448
+ _globals['_REGISTERREQUEST_LABELSENTRY']._serialized_start=391
+ _globals['_REGISTERREQUEST_LABELSENTRY']._serialized_end=448
+ _globals['_DRIVERINSTANCEREPORT']._serialized_start=451
+ _globals['_DRIVERINSTANCEREPORT']._serialized_end=917
+ _globals['_DRIVERINSTANCEREPORT_LABELSENTRY']._serialized_start=391
+ _globals['_DRIVERINSTANCEREPORT_LABELSENTRY']._serialized_end=448
+ _globals['_DRIVERINSTANCEREPORT_METHODSDESCRIPTIONENTRY']._serialized_start=816
+ _globals['_DRIVERINSTANCEREPORT_METHODSDESCRIPTIONENTRY']._serialized_end=885
+ _globals['_REGISTERRESPONSE']._serialized_start=919
+ _globals['_REGISTERRESPONSE']._serialized_end=957
+ _globals['_UNREGISTERREQUEST']._serialized_start=959
+ _globals['_UNREGISTERREQUEST']._serialized_end=1002
+ _globals['_UNREGISTERRESPONSE']._serialized_start=1004
+ _globals['_UNREGISTERRESPONSE']._serialized_end=1024
+ _globals['_LISTENREQUEST']._serialized_start=1026
+ _globals['_LISTENREQUEST']._serialized_end=1072
+ _globals['_LISTENRESPONSE']._serialized_start=1074
+ _globals['_LISTENRESPONSE']._serialized_end=1166
+ _globals['_STATUSREQUEST']._serialized_start=1168
+ _globals['_STATUSREQUEST']._serialized_end=1183
+ _globals['_STATUSRESPONSE']._serialized_start=1186
+ _globals['_STATUSRESPONSE']._serialized_end=1331
+ _globals['_DIALREQUEST']._serialized_start=1333
+ _globals['_DIALREQUEST']._serialized_end=1377
+ _globals['_DIALRESPONSE']._serialized_start=1379
+ _globals['_DIALRESPONSE']._serialized_end=1469
+ _globals['_AUDITSTREAMREQUEST']._serialized_start=1472
+ _globals['_AUDITSTREAMREQUEST']._serialized_end=1633
+ _globals['_REPORTSTATUSREQUEST']._serialized_start=1635
+ _globals['_REPORTSTATUSREQUEST']._serialized_end=1755
+ _globals['_REPORTSTATUSRESPONSE']._serialized_start=1757
+ _globals['_REPORTSTATUSRESPONSE']._serialized_end=1779
+ _globals['_GETREPORTRESPONSE']._serialized_start=1782
+ _globals['_GETREPORTRESPONSE']._serialized_end=2094
+ _globals['_GETREPORTRESPONSE_LABELSENTRY']._serialized_start=391
+ _globals['_GETREPORTRESPONSE_LABELSENTRY']._serialized_end=448
+ _globals['_ENDPOINT']._serialized_start=2097
+ _globals['_ENDPOINT']._serialized_end=2262
+ _globals['_DRIVERCALLREQUEST']._serialized_start=2264
+ _globals['_DRIVERCALLREQUEST']._serialized_end=2371
+ _globals['_DRIVERCALLRESPONSE']._serialized_start=2373
+ _globals['_DRIVERCALLRESPONSE']._serialized_end=2461
+ _globals['_STREAMINGDRIVERCALLREQUEST']._serialized_start=2463
+ _globals['_STREAMINGDRIVERCALLREQUEST']._serialized_end=2579
+ _globals['_STREAMINGDRIVERCALLRESPONSE']._serialized_start=2581
+ _globals['_STREAMINGDRIVERCALLRESPONSE']._serialized_end=2678
+ _globals['_LOGSTREAMRESPONSE']._serialized_start=2681
+ _globals['_LOGSTREAMRESPONSE']._serialized_end=2841
+ _globals['_RESETREQUEST']._serialized_start=2843
+ _globals['_RESETREQUEST']._serialized_end=2857
+ _globals['_RESETRESPONSE']._serialized_start=2859
+ _globals['_RESETRESPONSE']._serialized_end=2874
+ _globals['_GETLEASEREQUEST']._serialized_start=2876
+ _globals['_GETLEASEREQUEST']._serialized_end=2913
+ _globals['_GETLEASERESPONSE']._serialized_start=2916
+ _globals['_GETLEASERESPONSE']._serialized_end=3319
+ _globals['_REQUESTLEASEREQUEST']._serialized_start=3322
+ _globals['_REQUESTLEASEREQUEST']._serialized_end=3457
+ _globals['_REQUESTLEASERESPONSE']._serialized_start=3459
+ _globals['_REQUESTLEASERESPONSE']._serialized_end=3501
+ _globals['_RELEASELEASEREQUEST']._serialized_start=3503
+ _globals['_RELEASELEASEREQUEST']._serialized_end=3544
+ _globals['_RELEASELEASERESPONSE']._serialized_start=3546
+ _globals['_RELEASELEASERESPONSE']._serialized_end=3568
+ _globals['_LISTLEASESREQUEST']._serialized_start=3570
+ _globals['_LISTLEASESREQUEST']._serialized_end=3589
+ _globals['_LISTLEASESRESPONSE']._serialized_start=3591
+ _globals['_LISTLEASESRESPONSE']._serialized_end=3633
+ _globals['_GETSTATUSREQUEST']._serialized_start=3635
+ _globals['_GETSTATUSREQUEST']._serialized_end=3653
+ _globals['_GETSTATUSRESPONSE']._serialized_start=3655
+ _globals['_GETSTATUSRESPONSE']._serialized_end=3773
+ _globals['_CONTROLLERSERVICE']._serialized_start=3776
+ _globals['_CONTROLLERSERVICE']._serialized_end=4690
+ _globals['_EXPORTERSERVICE']._serialized_start=4693
+ _globals['_EXPORTERSERVICE']._serialized_end=5207
# @@protoc_insertion_point(module_scope)
diff --git a/packages/jumpstarter-protocol/jumpstarter_protocol/jumpstarter/v1/jumpstarter_pb2_grpc.py b/packages/jumpstarter-protocol/jumpstarter_protocol/jumpstarter/v1/jumpstarter_pb2_grpc.py
index d975d654f..641a345ba 100644
--- a/packages/jumpstarter-protocol/jumpstarter_protocol/jumpstarter/v1/jumpstarter_pb2_grpc.py
+++ b/packages/jumpstarter-protocol/jumpstarter_protocol/jumpstarter/v1/jumpstarter_pb2_grpc.py
@@ -26,6 +26,11 @@ def __init__(self, channel):
request_serializer=jumpstarter_dot_v1_dot_jumpstarter__pb2.UnregisterRequest.SerializeToString,
response_deserializer=jumpstarter_dot_v1_dot_jumpstarter__pb2.UnregisterResponse.FromString,
_registered_method=True)
+ self.ReportStatus = channel.unary_unary(
+ '/jumpstarter.v1.ControllerService/ReportStatus',
+ request_serializer=jumpstarter_dot_v1_dot_jumpstarter__pb2.ReportStatusRequest.SerializeToString,
+ response_deserializer=jumpstarter_dot_v1_dot_jumpstarter__pb2.ReportStatusResponse.FromString,
+ _registered_method=True)
self.Listen = channel.unary_stream(
'/jumpstarter.v1.ControllerService/Listen',
request_serializer=jumpstarter_dot_v1_dot_jumpstarter__pb2.ListenRequest.SerializeToString,
@@ -89,6 +94,14 @@ def Unregister(self, request, context):
context.set_details('Method not implemented!')
raise NotImplementedError('Method not implemented!')
+ def ReportStatus(self, request, context):
+ """Exporter status report
+ Allows exporters to report their own status to the controller
+ """
+ context.set_code(grpc.StatusCode.UNIMPLEMENTED)
+ context.set_details('Method not implemented!')
+ raise NotImplementedError('Method not implemented!')
+
def Listen(self, request, context):
"""Exporter listening
Returns stream tokens for accepting incoming client connections
@@ -163,6 +176,11 @@ def add_ControllerServiceServicer_to_server(servicer, server):
request_deserializer=jumpstarter_dot_v1_dot_jumpstarter__pb2.UnregisterRequest.FromString,
response_serializer=jumpstarter_dot_v1_dot_jumpstarter__pb2.UnregisterResponse.SerializeToString,
),
+ 'ReportStatus': grpc.unary_unary_rpc_method_handler(
+ servicer.ReportStatus,
+ request_deserializer=jumpstarter_dot_v1_dot_jumpstarter__pb2.ReportStatusRequest.FromString,
+ response_serializer=jumpstarter_dot_v1_dot_jumpstarter__pb2.ReportStatusResponse.SerializeToString,
+ ),
'Listen': grpc.unary_stream_rpc_method_handler(
servicer.Listen,
request_deserializer=jumpstarter_dot_v1_dot_jumpstarter__pb2.ListenRequest.FromString,
@@ -269,6 +287,33 @@ def Unregister(request,
metadata,
_registered_method=True)
+ @staticmethod
+ def ReportStatus(request,
+ target,
+ options=(),
+ channel_credentials=None,
+ call_credentials=None,
+ insecure=False,
+ compression=None,
+ wait_for_ready=None,
+ timeout=None,
+ metadata=None):
+ return grpc.experimental.unary_unary(
+ request,
+ target,
+ '/jumpstarter.v1.ControllerService/ReportStatus',
+ jumpstarter_dot_v1_dot_jumpstarter__pb2.ReportStatusRequest.SerializeToString,
+ jumpstarter_dot_v1_dot_jumpstarter__pb2.ReportStatusResponse.FromString,
+ options,
+ channel_credentials,
+ insecure,
+ call_credentials,
+ compression,
+ wait_for_ready,
+ timeout,
+ metadata,
+ _registered_method=True)
+
@staticmethod
def Listen(request,
target,
@@ -522,6 +567,11 @@ def __init__(self, channel):
request_serializer=jumpstarter_dot_v1_dot_jumpstarter__pb2.ResetRequest.SerializeToString,
response_deserializer=jumpstarter_dot_v1_dot_jumpstarter__pb2.ResetResponse.FromString,
_registered_method=True)
+ self.GetStatus = channel.unary_unary(
+ '/jumpstarter.v1.ExporterService/GetStatus',
+ request_serializer=jumpstarter_dot_v1_dot_jumpstarter__pb2.GetStatusRequest.SerializeToString,
+ response_deserializer=jumpstarter_dot_v1_dot_jumpstarter__pb2.GetStatusResponse.FromString,
+ _registered_method=True)
class ExporterServiceServicer(object):
@@ -560,6 +610,12 @@ def Reset(self, request, context):
context.set_details('Method not implemented!')
raise NotImplementedError('Method not implemented!')
+ def GetStatus(self, request, context):
+ """Missing associated documentation comment in .proto file."""
+ context.set_code(grpc.StatusCode.UNIMPLEMENTED)
+ context.set_details('Method not implemented!')
+ raise NotImplementedError('Method not implemented!')
+
def add_ExporterServiceServicer_to_server(servicer, server):
rpc_method_handlers = {
@@ -588,6 +644,11 @@ def add_ExporterServiceServicer_to_server(servicer, server):
request_deserializer=jumpstarter_dot_v1_dot_jumpstarter__pb2.ResetRequest.FromString,
response_serializer=jumpstarter_dot_v1_dot_jumpstarter__pb2.ResetResponse.SerializeToString,
),
+ 'GetStatus': grpc.unary_unary_rpc_method_handler(
+ servicer.GetStatus,
+ request_deserializer=jumpstarter_dot_v1_dot_jumpstarter__pb2.GetStatusRequest.FromString,
+ response_serializer=jumpstarter_dot_v1_dot_jumpstarter__pb2.GetStatusResponse.SerializeToString,
+ ),
}
generic_handler = grpc.method_handlers_generic_handler(
'jumpstarter.v1.ExporterService', rpc_method_handlers)
@@ -735,3 +796,30 @@ def Reset(request,
timeout,
metadata,
_registered_method=True)
+
+ @staticmethod
+ def GetStatus(request,
+ target,
+ options=(),
+ channel_credentials=None,
+ call_credentials=None,
+ insecure=False,
+ compression=None,
+ wait_for_ready=None,
+ timeout=None,
+ metadata=None):
+ return grpc.experimental.unary_unary(
+ request,
+ target,
+ '/jumpstarter.v1.ExporterService/GetStatus',
+ jumpstarter_dot_v1_dot_jumpstarter__pb2.GetStatusRequest.SerializeToString,
+ jumpstarter_dot_v1_dot_jumpstarter__pb2.GetStatusResponse.FromString,
+ options,
+ channel_credentials,
+ insecure,
+ call_credentials,
+ compression,
+ wait_for_ready,
+ timeout,
+ metadata,
+ _registered_method=True)
diff --git a/packages/jumpstarter-protocol/jumpstarter_protocol/jumpstarter/v1/kubernetes_pb2.py b/packages/jumpstarter-protocol/jumpstarter_protocol/jumpstarter/v1/kubernetes_pb2.py
index d7e6db9b4..fc7fcc649 100644
--- a/packages/jumpstarter-protocol/jumpstarter_protocol/jumpstarter/v1/kubernetes_pb2.py
+++ b/packages/jumpstarter-protocol/jumpstarter_protocol/jumpstarter/v1/kubernetes_pb2.py
@@ -24,14 +24,13 @@
-DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x1fjumpstarter/v1/kubernetes.proto\x12\x0ejumpstarter.v1\"`\n\x18LabelSelectorRequirement\x12\x10\n\x03key\x18\x01 \x01(\tR\x03key\x12\x1a\n\x08operator\x18\x02 \x01(\tR\x08operator\x12\x16\n\x06values\x18\x03 \x03(\tR\x06values\"\xf9\x01\n\rLabelSelector\x12U\n\x11match_expressions\x18\x01 \x03(\x0b\x32(.jumpstarter.v1.LabelSelectorRequirementR\x10matchExpressions\x12Q\n\x0cmatch_labels\x18\x02 \x03(\x0b\x32..jumpstarter.v1.LabelSelector.MatchLabelsEntryR\x0bmatchLabels\x1a>\n\x10MatchLabelsEntry\x12\x10\n\x03key\x18\x01 \x01(\tR\x03key\x12\x14\n\x05value\x18\x02 \x01(\tR\x05value:\x02\x38\x01\"V\n\x04Time\x12\x1d\n\x07seconds\x18\x01 \x01(\x03H\x00R\x07seconds\x88\x01\x01\x12\x19\n\x05nanos\x18\x02 \x01(\x05H\x01R\x05nanos\x88\x01\x01\x42\n\n\x08_secondsB\x08\n\x06_nanos\"\xd6\x02\n\tCondition\x12\x17\n\x04type\x18\x01 \x01(\tH\x00R\x04type\x88\x01\x01\x12\x1b\n\x06status\x18\x02 \x01(\tH\x01R\x06status\x88\x01\x01\x12\x33\n\x12observedGeneration\x18\x03 \x01(\x03H\x02R\x12observedGeneration\x88\x01\x01\x12I\n\x12lastTransitionTime\x18\x04 \x01(\x0b\x32\x14.jumpstarter.v1.TimeH\x03R\x12lastTransitionTime\x88\x01\x01\x12\x1b\n\x06reason\x18\x05 \x01(\tH\x04R\x06reason\x88\x01\x01\x12\x1d\n\x07message\x18\x06 \x01(\tH\x05R\x07message\x88\x01\x01\x42\x07\n\x05_typeB\t\n\x07_statusB\x15\n\x13_observedGenerationB\x15\n\x13_lastTransitionTimeB\t\n\x07_reasonB\n\n\x08_messageB~\n\x12\x63om.jumpstarter.v1B\x0fKubernetesProtoP\x01\xa2\x02\x03JXX\xaa\x02\x0eJumpstarter.V1\xca\x02\x0eJumpstarter\\V1\xe2\x02\x1aJumpstarter\\V1\\GPBMetadata\xea\x02\x0fJumpstarter::V1b\x06proto3')
+DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x1fjumpstarter/v1/kubernetes.proto\x12\x0ejumpstarter.v1\"`\n\x18LabelSelectorRequirement\x12\x10\n\x03key\x18\x01 \x01(\tR\x03key\x12\x1a\n\x08operator\x18\x02 \x01(\tR\x08operator\x12\x16\n\x06values\x18\x03 \x03(\tR\x06values\"\xf9\x01\n\rLabelSelector\x12U\n\x11match_expressions\x18\x01 \x03(\x0b\x32(.jumpstarter.v1.LabelSelectorRequirementR\x10matchExpressions\x12Q\n\x0cmatch_labels\x18\x02 \x03(\x0b\x32..jumpstarter.v1.LabelSelector.MatchLabelsEntryR\x0bmatchLabels\x1a>\n\x10MatchLabelsEntry\x12\x10\n\x03key\x18\x01 \x01(\tR\x03key\x12\x14\n\x05value\x18\x02 \x01(\tR\x05value:\x02\x38\x01\"V\n\x04Time\x12\x1d\n\x07seconds\x18\x01 \x01(\x03H\x00R\x07seconds\x88\x01\x01\x12\x19\n\x05nanos\x18\x02 \x01(\x05H\x01R\x05nanos\x88\x01\x01\x42\n\n\x08_secondsB\x08\n\x06_nanos\"\xd6\x02\n\tCondition\x12\x17\n\x04type\x18\x01 \x01(\tH\x00R\x04type\x88\x01\x01\x12\x1b\n\x06status\x18\x02 \x01(\tH\x01R\x06status\x88\x01\x01\x12\x33\n\x12observedGeneration\x18\x03 \x01(\x03H\x02R\x12observedGeneration\x88\x01\x01\x12I\n\x12lastTransitionTime\x18\x04 \x01(\x0b\x32\x14.jumpstarter.v1.TimeH\x03R\x12lastTransitionTime\x88\x01\x01\x12\x1b\n\x06reason\x18\x05 \x01(\tH\x04R\x06reason\x88\x01\x01\x12\x1d\n\x07message\x18\x06 \x01(\tH\x05R\x07message\x88\x01\x01\x42\x07\n\x05_typeB\t\n\x07_statusB\x15\n\x13_observedGenerationB\x15\n\x13_lastTransitionTimeB\t\n\x07_reasonB\n\n\x08_messageb\x06proto3')
_globals = globals()
_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals)
_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'jumpstarter.v1.kubernetes_pb2', _globals)
if not _descriptor._USE_C_DESCRIPTORS:
- _globals['DESCRIPTOR']._loaded_options = None
- _globals['DESCRIPTOR']._serialized_options = b'\n\022com.jumpstarter.v1B\017KubernetesProtoP\001\242\002\003JXX\252\002\016Jumpstarter.V1\312\002\016Jumpstarter\\V1\342\002\032Jumpstarter\\V1\\GPBMetadata\352\002\017Jumpstarter::V1'
+ DESCRIPTOR._loaded_options = None
_globals['_LABELSELECTOR_MATCHLABELSENTRY']._loaded_options = None
_globals['_LABELSELECTOR_MATCHLABELSENTRY']._serialized_options = b'8\001'
_globals['_LABELSELECTORREQUIREMENT']._serialized_start=51
diff --git a/packages/jumpstarter-protocol/jumpstarter_protocol/jumpstarter/v1/router_pb2.py b/packages/jumpstarter-protocol/jumpstarter_protocol/jumpstarter/v1/router_pb2.py
index 0d86b315a..030a29cbb 100644
--- a/packages/jumpstarter-protocol/jumpstarter_protocol/jumpstarter/v1/router_pb2.py
+++ b/packages/jumpstarter-protocol/jumpstarter_protocol/jumpstarter/v1/router_pb2.py
@@ -24,14 +24,13 @@
-DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x1bjumpstarter/v1/router.proto\x12\x0ejumpstarter.v1\"c\n\rStreamRequest\x12\x18\n\x07payload\x18\x01 \x01(\x0cR\x07payload\x12\x38\n\nframe_type\x18\x02 \x01(\x0e\x32\x19.jumpstarter.v1.FrameTypeR\tframeType\"d\n\x0eStreamResponse\x12\x18\n\x07payload\x18\x01 \x01(\x0cR\x07payload\x12\x38\n\nframe_type\x18\x02 \x01(\x0e\x32\x19.jumpstarter.v1.FrameTypeR\tframeType*g\n\tFrameType\x12\x13\n\x0f\x46RAME_TYPE_DATA\x10\x00\x12\x19\n\x15\x46RAME_TYPE_RST_STREAM\x10\x03\x12\x13\n\x0f\x46RAME_TYPE_PING\x10\x06\x12\x15\n\x11\x46RAME_TYPE_GOAWAY\x10\x07\x32\\\n\rRouterService\x12K\n\x06Stream\x12\x1d.jumpstarter.v1.StreamRequest\x1a\x1e.jumpstarter.v1.StreamResponse(\x01\x30\x01\x42z\n\x12\x63om.jumpstarter.v1B\x0bRouterProtoP\x01\xa2\x02\x03JXX\xaa\x02\x0eJumpstarter.V1\xca\x02\x0eJumpstarter\\V1\xe2\x02\x1aJumpstarter\\V1\\GPBMetadata\xea\x02\x0fJumpstarter::V1b\x06proto3')
+DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x1bjumpstarter/v1/router.proto\x12\x0ejumpstarter.v1\"c\n\rStreamRequest\x12\x18\n\x07payload\x18\x01 \x01(\x0cR\x07payload\x12\x38\n\nframe_type\x18\x02 \x01(\x0e\x32\x19.jumpstarter.v1.FrameTypeR\tframeType\"d\n\x0eStreamResponse\x12\x18\n\x07payload\x18\x01 \x01(\x0cR\x07payload\x12\x38\n\nframe_type\x18\x02 \x01(\x0e\x32\x19.jumpstarter.v1.FrameTypeR\tframeType*g\n\tFrameType\x12\x13\n\x0f\x46RAME_TYPE_DATA\x10\x00\x12\x19\n\x15\x46RAME_TYPE_RST_STREAM\x10\x03\x12\x13\n\x0f\x46RAME_TYPE_PING\x10\x06\x12\x15\n\x11\x46RAME_TYPE_GOAWAY\x10\x07\x32\\\n\rRouterService\x12K\n\x06Stream\x12\x1d.jumpstarter.v1.StreamRequest\x1a\x1e.jumpstarter.v1.StreamResponse(\x01\x30\x01\x62\x06proto3')
_globals = globals()
_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals)
_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'jumpstarter.v1.router_pb2', _globals)
if not _descriptor._USE_C_DESCRIPTORS:
- _globals['DESCRIPTOR']._loaded_options = None
- _globals['DESCRIPTOR']._serialized_options = b'\n\022com.jumpstarter.v1B\013RouterProtoP\001\242\002\003JXX\252\002\016Jumpstarter.V1\312\002\016Jumpstarter\\V1\342\002\032Jumpstarter\\V1\\GPBMetadata\352\002\017Jumpstarter::V1'
+ DESCRIPTOR._loaded_options = None
_globals['_FRAMETYPE']._serialized_start=250
_globals['_FRAMETYPE']._serialized_end=353
_globals['_STREAMREQUEST']._serialized_start=47
diff --git a/packages/jumpstarter/jumpstarter/client/base.py b/packages/jumpstarter/jumpstarter/client/base.py
index 1616d4c02..1420c52da 100644
--- a/packages/jumpstarter/jumpstarter/client/base.py
+++ b/packages/jumpstarter/jumpstarter/client/base.py
@@ -33,6 +33,12 @@ class DriverClient(AsyncDriverClient):
portal: BlockingPortal
stack: ExitStack
+ description: str | None = None
+ """Driver description from GetReport(), used for CLI help text"""
+
+ methods_description: dict[str, str] = field(default_factory=dict)
+ """Map of method names to their help descriptions from GetReport()"""
+
def call(self, method, *args):
"""
Invoke driver call
diff --git a/packages/jumpstarter/jumpstarter/client/client.py b/packages/jumpstarter/jumpstarter/client/client.py
index e77f40a75..f4aaef824 100644
--- a/packages/jumpstarter/jumpstarter/client/client.py
+++ b/packages/jumpstarter/jumpstarter/client/client.py
@@ -51,6 +51,7 @@ async def client_from_channel(
report = reports[index]
client_class = import_class(report.labels["jumpstarter.dev/client"], allow, unsafe)
+
client = client_class(
uuid=UUID(report.uuid),
labels=report.labels,
@@ -58,6 +59,8 @@ async def client_from_channel(
portal=portal,
stack=stack.enter_context(ExitStack()),
children={reports[k].labels["jumpstarter.dev/name"]: clients[k] for k in topo[index]},
+ description=getattr(report, 'description', None) or None,
+ methods_description=getattr(report, 'methods_description', {}) or {},
)
clients[index] = client
diff --git a/packages/jumpstarter/jumpstarter/client/decorators.py b/packages/jumpstarter/jumpstarter/client/decorators.py
new file mode 100644
index 000000000..2343f8355
--- /dev/null
+++ b/packages/jumpstarter/jumpstarter/client/decorators.py
@@ -0,0 +1,102 @@
+"""
+Client-side Click group helpers for building driver CLIs.
+"""
+
+from typing import TYPE_CHECKING, Any, Callable
+
+import click
+
+if TYPE_CHECKING:
+ from jumpstarter.client import DriverClient
+
+
+def driver_click_group(client: "DriverClient", **kwargs: Any) -> Callable:
+ """
+ Decorator factory for multi-command driver groups.
+
+ Allows server-side description override, otherwise uses Click's default behavior.
+
+ Usage:
+ def cli(self):
+ @driver_click_group(self)
+ def base():
+ '''Generic power interface''' # ← Click uses this by default
+ pass
+
+ @base.command()
+ def on():
+ '''Power on'''
+ self.on()
+
+ return base
+
+ :param client: DriverClient instance (provides description and methods_description)
+ :param kwargs: Keyword arguments passed to DriverClickGroup
+ :return: Decorator that creates a DriverClickGroup
+ """
+ def decorator(f: Callable) -> DriverClickGroup:
+ # Use function docstring if no help= provided
+ if 'help' not in kwargs or kwargs['help'] is None:
+ if f.__doc__:
+ kwargs['help'] = f.__doc__.strip()
+
+ # Server description overrides Click defaults
+ if getattr(client, 'description', None):
+ kwargs['help'] = client.description
+
+ group = DriverClickGroup(client, name=f.__name__, callback=f, **kwargs)
+
+ # Transfer Click parameters attached by decorators like @click.option
+ group.params = getattr(f, '__click_params__', [])
+
+ return group
+
+ return decorator
+
+
+def driver_click_command(client: "DriverClient", **kwargs: Any) -> Callable:
+ """
+ Decorator factory for single-command drivers (e.g., SSH, TMT).
+
+ Allows server-side description override, otherwise uses Click's default behavior.
+
+ Usage:
+ def cli(self):
+ @driver_click_command(self)
+ @click.argument("args", nargs=-1)
+ def ssh(args):
+ '''Run SSH command''' # ← Click uses this by default
+ ...
+ return ssh
+
+ :param client: DriverClient instance (provides description field)
+ :param kwargs: Keyword arguments passed to click.command
+ :return: click.command decorator
+ """
+ # Server description overrides Click's defaults (help= parameter or docstring)
+ if getattr(client, 'description', None):
+ kwargs['help'] = client.description
+
+ return click.command(**kwargs)
+
+
+class DriverClickGroup(click.Group):
+ """Click Group with server-configurable help text via methods_description."""
+
+ def __init__(self, client: "DriverClient", *args: Any, **kwargs: Any) -> None:
+ super().__init__(*args, **kwargs)
+ self.client = client
+
+ def command(self, *args: Any, **kwargs: Any) -> Callable:
+ """Command decorator with server methods_description override support."""
+ def decorator(f: Callable) -> click.Command:
+ name = kwargs.get('name')
+ if not name:
+ name = f.__name__.lower().replace('_', '-')
+
+ if name in self.client.methods_description:
+ kwargs['help'] = self.client.methods_description[name]
+
+ return super(DriverClickGroup, self).command(*args, **kwargs)(f)
+
+ return decorator
diff --git a/packages/jumpstarter/jumpstarter/config/exporter.py b/packages/jumpstarter/jumpstarter/config/exporter.py
index 8cc8b9bd6..efd4724b6 100644
--- a/packages/jumpstarter/jumpstarter/config/exporter.py
+++ b/packages/jumpstarter/jumpstarter/config/exporter.py
@@ -28,6 +28,8 @@ class ExporterConfigV1Alpha1DriverInstanceComposite(BaseModel):
class ExporterConfigV1Alpha1DriverInstanceBase(BaseModel):
type: str
+ description: str | None = None
+ methods_description: dict[str, str] = Field(default_factory=dict)
config: dict[str, Any] = Field(default_factory=dict)
children: dict[str, ExporterConfigV1Alpha1DriverInstance] = Field(default_factory=dict)
@@ -46,7 +48,12 @@ def instantiate(self) -> Driver:
children = {name: child.instantiate() for name, child in self.root.children.items()}
- return driver_class(children=children, **self.root.config)
+ return driver_class(
+ description=self.root.description,
+ methods_description=self.root.methods_description,
+ children=children,
+ **self.root.config
+ )
case ExporterConfigV1Alpha1DriverInstanceComposite():
from jumpstarter_driver_composite.driver import Composite
@@ -84,6 +91,7 @@ class ExporterConfigV1Alpha1(BaseModel):
token: str | None = Field(default=None)
grpcOptions: dict[str, str | int] | None = Field(default_factory=dict)
+ description: str | None = None
export: dict[str, ExporterConfigV1Alpha1DriverInstance] = Field(default_factory=dict)
path: Path | None = Field(default=None)
@@ -145,7 +153,11 @@ async def serve_unix_async(self):
from jumpstarter.exporter import Session
with Session(
- root_device=ExporterConfigV1Alpha1DriverInstance(children=self.export).instantiate(),
+ root_device=ExporterConfigV1Alpha1DriverInstance(
+ type="jumpstarter_driver_composite.driver.Composite",
+ description=self.description,
+ children=self.export,
+ ).instantiate(),
) as session:
async with session.serve_unix_async() as path:
yield path
@@ -178,7 +190,11 @@ async def channel_factory():
try:
exporter = Exporter(
channel_factory=channel_factory,
- device_factory=ExporterConfigV1Alpha1DriverInstance(children=self.export).instantiate,
+ device_factory=ExporterConfigV1Alpha1DriverInstance(
+ type="jumpstarter_driver_composite.driver.Composite",
+ description=self.description,
+ children=self.export,
+ ).instantiate,
tls=self.tls,
grpc_options=self.grpcOptions,
)
diff --git a/packages/jumpstarter/jumpstarter/driver/base.py b/packages/jumpstarter/jumpstarter/driver/base.py
index eedb92a01..895b007c2 100644
--- a/packages/jumpstarter/jumpstarter/driver/base.py
+++ b/packages/jumpstarter/jumpstarter/driver/base.py
@@ -72,6 +72,12 @@ class Driver(
resources: dict[UUID, Any] = field(default_factory=dict, init=False)
"""Dict of client side resources"""
+ description: str | None = None
+ """Custom description for the driver (shown in CLI help)"""
+
+ methods_description: dict[str, str] = field(default_factory=dict)
+ """Map of method names to their help descriptions (configurable via server config)"""
+
log_level: str = "INFO"
logger: logging.Logger = field(init=False)
@@ -206,6 +212,8 @@ def report(self, *, root=None, parent=None, name=None):
| self.extra_labels()
| ({"jumpstarter.dev/client": self.client()})
| ({"jumpstarter.dev/name": name} if name else {}),
+ description=self.description or None,
+ methods_description=self.methods_description or {},
)
def enumerate(self, *, root=None, parent=None, name=None):
diff --git a/packages/jumpstarter/jumpstarter/exporter/session_test.py b/packages/jumpstarter/jumpstarter/exporter/session_test.py
new file mode 100644
index 000000000..664a2a02e
--- /dev/null
+++ b/packages/jumpstarter/jumpstarter/exporter/session_test.py
@@ -0,0 +1,312 @@
+"""Tests for session GetReport with descriptions and methods_description"""
+
+from google.protobuf import empty_pb2
+
+from jumpstarter.common.utils import serve
+from jumpstarter.driver import Driver
+
+
+class SimpleDriver(Driver):
+ """Simple test driver"""
+
+ @classmethod
+ def client(cls):
+ return "jumpstarter.client.DriverClient"
+
+
+class CompositeDriver_(Driver):
+ """Simple composite driver for testing"""
+
+ @classmethod
+ def client(cls):
+ return "jumpstarter.client.DriverClient"
+
+
+def test_get_report_includes_descriptions():
+ """Test that GetReport includes descriptions for drivers that have them"""
+ # Create drivers with and without descriptions
+ driver_with_desc = SimpleDriver(description="Custom test driver")
+ driver_without_desc = SimpleDriver()
+
+ root = CompositeDriver_(
+ children={
+ "with_desc": driver_with_desc,
+ "without_desc": driver_without_desc,
+ }
+ )
+
+ with serve(root) as _:
+ # Get the raw report response
+
+ from jumpstarter.exporter.session import Session
+
+ # Create session manually to access GetReport
+ session = Session(
+ uuid=root.uuid,
+ labels=root.labels,
+ root_device=root,
+ )
+
+ # Call GetReport
+ import asyncio
+ response = asyncio.run(session.GetReport(empty_pb2.Empty(), None))
+
+ # Build a map of uuid -> report for easy lookup
+ reports_by_uuid = {r.uuid: r for r in response.reports}
+
+ # Verify driver with description has it in its report
+ assert str(driver_with_desc.uuid) in reports_by_uuid
+ report_with_desc = reports_by_uuid[str(driver_with_desc.uuid)]
+ assert hasattr(report_with_desc, 'description')
+ assert report_with_desc.description == "Custom test driver"
+
+ # Verify driver without description doesn't have the field set
+ assert str(driver_without_desc.uuid) in reports_by_uuid
+ report_without_desc = reports_by_uuid[str(driver_without_desc.uuid)]
+ # Optional field - either not set or empty string
+ assert not getattr(report_without_desc, 'description', None)
+
+
+def test_client_receives_description():
+ """Test that client receives description from GetReport"""
+ driver = SimpleDriver(description="Test description")
+
+ with serve(driver) as client:
+ # Description is passed during init from GetReport
+ assert client.description == "Test description"
+
+
+def test_cli_uses_description_or_default():
+ """Test that CLI uses description from GetReport or falls back to default"""
+ # Test with description set
+ driver_with_desc = SimpleDriver(description="Custom CLI description")
+ with serve(driver_with_desc) as client:
+ # Simulate what cli() method would do
+ help_text = client.description or "Default help text"
+ assert help_text == "Custom CLI description"
+
+ # Test without description
+ driver_without_desc = SimpleDriver()
+ with serve(driver_without_desc) as client:
+ help_text = client.description or "Default help text"
+ assert help_text == "Default help text"
+
+
+def test_multiple_drivers_with_descriptions():
+ """Test that multiple drivers can have different descriptions"""
+ power = SimpleDriver(description="Power control")
+ serial = SimpleDriver(description="Serial communication")
+ storage = SimpleDriver(description="Storage management")
+ plain = SimpleDriver() # No description
+
+ root = CompositeDriver_(
+ children={
+ "power": power,
+ "serial": serial,
+ "storage": storage,
+ "plain": plain,
+ }
+ )
+
+ with serve(root) as client:
+ # Each child should have its description from GetReport
+ assert client.children['power'].description == "Power control"
+ assert client.children['serial'].description == "Serial communication"
+ assert client.children['storage'].description == "Storage management"
+ assert client.children['plain'].description is None
+
+
+def test_empty_description_not_included():
+ """Test that empty strings are not included in descriptions map"""
+ driver = SimpleDriver(description="")
+
+ with serve(driver) as _:
+ from jumpstarter.exporter.session import Session
+
+ session = Session(
+ uuid=driver.uuid,
+ labels=driver.labels,
+ root_device=driver,
+ )
+
+ import asyncio
+ response = asyncio.run(session.GetReport(empty_pb2.Empty(), None))
+
+ # Empty string should not be included in the report
+ reports_by_uuid = {r.uuid: r for r in response.reports}
+ assert str(driver.uuid) in reports_by_uuid
+ report = reports_by_uuid[str(driver.uuid)]
+ # Empty description should not be set
+ assert not getattr(report, 'description', None)
+
+
+def test_description_override_in_exporter_config():
+ """Test that description in exporter config overrides default"""
+ # Create a driver with a custom description
+ custom_driver = SimpleDriver(description="Custom override description")
+
+ with serve(custom_driver) as client:
+ # Client should receive the custom description
+ assert client.description == "Custom override description"
+
+
+def test_description_available_to_cli():
+ """Test that description is available for CLI group help text"""
+ # Test with custom description
+ driver_with_desc = SimpleDriver(description="Power management interface")
+ with serve(driver_with_desc) as client:
+ # Description should be available for CLI
+ assert client.description == "Power management interface"
+
+ # This is what DriverClickGroup would use
+ cli_help = client.description or "Default CLI help"
+ assert cli_help == "Power management interface"
+
+ # Test without description (falls back to default)
+ driver_no_desc = SimpleDriver()
+ with serve(driver_no_desc) as client:
+ assert client.description is None
+
+ # DriverClickGroup falls back to provided default
+ cli_help = client.description or "Default CLI help"
+ assert cli_help == "Default CLI help"
+
+
+def test_composite_children_each_have_own_description():
+ """Test that each child in composite can have its own description"""
+ power = SimpleDriver(description="Power control interface")
+ serial = SimpleDriver(description="Serial communication interface")
+ storage = SimpleDriver(description="Storage management interface")
+ network = SimpleDriver() # No custom description
+
+ root = CompositeDriver_(
+ description="Main composite device",
+ children={
+ "power": power,
+ "serial": serial,
+ "storage": storage,
+ "network": network,
+ }
+ )
+
+ with serve(root) as client:
+ # Root has its own description
+ assert client.description == "Main composite device"
+
+ # Each child maintains its own description
+ assert client.children['power'].description == "Power control interface"
+ assert client.children['serial'].description == "Serial communication interface"
+ assert client.children['storage'].description == "Storage management interface"
+ assert client.children['network'].description is None
+
+
+def test_methods_description_set_via_config():
+ """Test that methods_description can be set via server configuration"""
+ # Server can override method descriptions via config
+ driver = SimpleDriver(
+ description="Power management",
+ methods_description={
+ "on": "Custom: Turn device power on",
+ "off": "Custom: Turn device power off",
+ "cycle": "Custom: Power cycle the device"
+ }
+ )
+
+ # methods_description should be set
+ assert "on" in driver.methods_description
+ assert driver.methods_description["on"] == "Custom: Turn device power on"
+ assert "off" in driver.methods_description
+ assert driver.methods_description["off"] == "Custom: Turn device power off"
+
+
+def test_methods_description_included_in_getreport():
+ """Test that GetReport includes methods_description for drivers"""
+ driver = SimpleDriver(
+ methods_description={
+ "on": "Turn the device on",
+ "off": "Turn the device off",
+ }
+ )
+
+ with serve(driver) as _:
+ from jumpstarter.exporter.session import Session
+
+ session = Session(
+ uuid=driver.uuid,
+ labels=driver.labels,
+ root_device=driver,
+ )
+
+ import asyncio
+ response = asyncio.run(session.GetReport(empty_pb2.Empty(), None))
+
+ # Find the driver's report
+ reports_by_uuid = {r.uuid: r for r in response.reports}
+ assert str(driver.uuid) in reports_by_uuid
+ report = reports_by_uuid[str(driver.uuid)]
+
+ # Verify methods_description is in the report
+ assert hasattr(report, 'methods_description')
+ assert "on" in report.methods_description
+ assert report.methods_description["on"] == "Turn the device on"
+ assert "off" in report.methods_description
+ assert report.methods_description["off"] == "Turn the device off"
+
+
+def test_client_receives_methods_description():
+ """Test that client receives methods_description from GetReport"""
+ driver = SimpleDriver(
+ description="Test power driver",
+ methods_description={
+ "on": "Turn the device on",
+ "off": "Turn the device off",
+ "read": "Stream power readings"
+ }
+ )
+
+ with serve(driver) as client:
+ # Client should have methods_description populated
+ assert "on" in client.methods_description
+ assert client.methods_description["on"] == "Turn the device on"
+ assert "off" in client.methods_description
+ assert client.methods_description["off"] == "Turn the device off"
+ assert "read" in client.methods_description
+ assert client.methods_description["read"] == "Stream power readings"
+
+
+def test_driverclickgroup_uses_methods_description_as_override():
+ """Test that DriverClickGroup uses methods_description to override client defaults"""
+ driver = SimpleDriver(
+ description="Power management",
+ methods_description={
+ "on": "Server override: Power on",
+ }
+ )
+
+ with serve(driver) as client:
+ # Simulate what DriverClickGroup.command() does:
+ # Priority: server methods_description > client help= > empty
+
+ # Method with server override
+ method_name = "on"
+ if method_name in client.methods_description:
+ help_text = client.methods_description[method_name]
+ elif "help" in {}: # Simulate client's help= parameter
+ help_text = {}["help"]
+ else:
+ help_text = ""
+
+ # Should get server override
+ assert help_text == "Server override: Power on"
+
+ # Method without server override
+ method_name = "off"
+ client_help = "Client default: Power off"
+ if method_name in client.methods_description:
+ help_text = client.methods_description[method_name]
+ else:
+ help_text = client_help
+
+ # Should fall back to client default
+ assert help_text == "Client default: Power off"
+