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" +