-
Notifications
You must be signed in to change notification settings - Fork 5
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
This drivers enables shell methods on exporters, so local tools can be used remotelly when no specific drivers are available.
- Loading branch information
Showing
10 changed files
with
269 additions
and
1 deletion.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
__pycache__/ | ||
.coverage | ||
coverage.xml |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
# Jumpstarter Driver for shell access | ||
|
||
This driver provides a simple shell access to the target exporter, and it is | ||
intended to be used when command line tools exist to manage existing interfaces | ||
or hardware, but no drivers exist yet in Jumpstarter. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,17 @@ | ||
apiVersion: jumpstarter.dev/v1alpha1 | ||
kind: ExporterConfig | ||
endpoint: grpc.jumpstarter.192.168.0.203.nip.io:8082 | ||
token: "<token>" | ||
export: | ||
example: | ||
type: jumpstarter_driver_shell.driver.Shell | ||
config: | ||
methods: | ||
ls: "ls" | ||
method2: "echo 'Hello World 2'" | ||
#multi line method | ||
method3: | | ||
echo 'Hello World $1' | ||
echo 'Hello World $2' | ||
env_var: "echo $ENV_VAR" | ||
|
Empty file.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,26 @@ | ||
from dataclasses import dataclass | ||
|
||
from jumpstarter.client import DriverClient | ||
|
||
|
||
@dataclass(kw_only=True) | ||
class ShellClient(DriverClient): | ||
_methods: list[str] = None | ||
|
||
""" | ||
Client interface for Shell driver. | ||
This client dynamically checks that the method is configured | ||
on the driver, and if it is, it will call it and get the results | ||
in the form of (stdout, stderr, returncode). | ||
""" | ||
def _check_method_exists(self, method): | ||
if self._methods is None: | ||
self._methods = self.call("get_methods") | ||
if method not in self._methods: | ||
raise AttributeError(f"method {method} not found in {self._methods}") | ||
|
||
## capture any method calls dynamically | ||
def __getattr__(self, name): | ||
self._check_method_exists(name) | ||
return lambda *args, **kwargs: tuple(self.call("call_method", name, kwargs, *args)) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,79 @@ | ||
import logging | ||
import os | ||
import subprocess | ||
from dataclasses import dataclass, field | ||
|
||
from jumpstarter.driver import Driver, export | ||
|
||
logger = logging.getLogger(__name__) | ||
|
||
@dataclass(kw_only=True) | ||
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] | ||
shell: list[str] = field(default_factory=lambda: ["bash", "-c"]) | ||
log_level: str = "INFO" | ||
cwd: str | None = None | ||
|
||
def __post_init__(self): | ||
super().__post_init__() | ||
# set logger log level | ||
logger.setLevel(self.log_level) | ||
|
||
@classmethod | ||
def client(cls) -> str: | ||
return "jumpstarter_driver_shell.client.ShellClient" | ||
|
||
@export | ||
def get_methods(self) -> list[str]: | ||
methods = list(self.methods.keys()) | ||
logger.debug(f"get_methods called, returning methods: {methods}") | ||
return methods | ||
|
||
@export | ||
def call_method(self, method: str, env, *args): | ||
logger.info(f"calling {method} with args: {args} and kwargs as env: {env}") | ||
script = self.methods[method] | ||
logger.debug(f"running script: {script}") | ||
result = self._run_inline_shell_script(method, script, *args, env_vars=env) | ||
if result.returncode != 0: | ||
logger.info(f"{method} return code: {result.returncode}") | ||
if result.stderr != "": | ||
logger.debug(f"{method} stderr:\n{result.stderr.rstrip("\n")}") | ||
if result.stdout != "": | ||
logger.debug(f"{method} stdout:\n{result.stdout.rstrip("\n")}") | ||
return result.stdout, result.stderr, result.returncode | ||
|
||
def _run_inline_shell_script(self, method, script, *args, env_vars=None): | ||
""" | ||
Run the given shell script (as a string) with optional arguments and | ||
environment variables. Returns a CompletedProcess with stdout, stderr, and returncode. | ||
: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. | ||
:return: A subprocess.CompletedProcess object (Python 3.5+). | ||
""" | ||
|
||
# Merge parent environment with the user-supplied env_vars | ||
# so that we don't lose existing environment variables. | ||
combined_env = os.environ.copy() | ||
if env_vars: | ||
combined_env.update(env_vars) | ||
|
||
cmd = self.shell + [script, method] + list(args) | ||
|
||
# Run the command | ||
result = subprocess.run( | ||
cmd, | ||
capture_output=True, # Captures stdout and stderr | ||
text=True, # Returns stdout/stderr as strings (not bytes) | ||
env=combined_env, # Pass our merged environment | ||
cwd=self.cwd # Run in the working directory (if set) | ||
) | ||
|
||
return result |
41 changes: 41 additions & 0 deletions
41
contrib/drivers/shell/jumpstarter_driver_shell/driver_test.py
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,41 @@ | ||
import pytest | ||
|
||
from jumpstarter.common.utils import serve | ||
|
||
from .driver import Shell | ||
|
||
|
||
@pytest.fixture | ||
def client(): | ||
instance = Shell(log_level = "DEBUG", | ||
methods={"echo": "echo $1", | ||
"env": "echo $ENV1", | ||
"multi_line": "echo $1\necho $2\necho $3", | ||
"exit1": "exit 1", | ||
"stderr": "echo $1 >&2"}) | ||
with serve(instance) as client: | ||
yield client | ||
|
||
def test_normal_args(client): | ||
assert client.echo("hello") == ("hello\n", "", 0) | ||
|
||
|
||
def test_env_vars(client): | ||
assert client.env(ENV1="world") == ("world\n", "", 0) | ||
|
||
def test_multi_line_scripts(client): | ||
assert client.multi_line("a", "b", "c") == ("a\nb\nc\n", "", 0) | ||
|
||
def test_return_codes(client): | ||
assert client.exit1() == ("", "", 1) | ||
|
||
def test_stderr(client): | ||
assert client.stderr("error") == ("", "error\n", 0) | ||
|
||
def test_unknown_method(client): | ||
try: | ||
client.unknown() | ||
except AttributeError as e: | ||
assert "method unknown not found in" in str(e) | ||
else: | ||
raise AssertionError("Expected AttributeError") |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,38 @@ | ||
[project] | ||
name = "jumpstarter-driver-shell" | ||
version = "0.1.0" | ||
description = "Add your description here" | ||
readme = "README.md" | ||
authors = [ | ||
{ name = "Miguel Angel Ajo", email = "[email protected]" } | ||
] | ||
requires-python = ">=3.11" | ||
dependencies = [ | ||
"anyio>=4.6.2.post1", | ||
"jumpstarter", | ||
] | ||
|
||
[tool.hatch.version] | ||
source = "vcs" | ||
raw-options = { 'root' = '../../../'} | ||
|
||
[tool.hatch.metadata.hooks.vcs.urls] | ||
Homepage = "https://jumpstarter.dev" | ||
source_archive = "https://github.com/jumpstarter-dev/repo/archive/{commit_hash}.zip" | ||
|
||
[tool.pytest.ini_options] | ||
addopts = "--cov --cov-report=html --cov-report=xml" | ||
log_cli = true | ||
log_cli_level = "INFO" | ||
testpaths = ["jumpstarter_driver_shell"] | ||
|
||
[build-system] | ||
requires = ["hatchling", "hatch-vcs"] | ||
build-backend = "hatchling.build" | ||
|
||
[dependency-groups] | ||
dev = [ | ||
"pytest-cov>=6.0.0", | ||
"pytest>=8.3.3", | ||
"ruff>=0.7.1", | ||
] |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,58 @@ | ||
# Shell driver | ||
|
||
**driver**: `jumpstarter_driver_shell.driver.Shell` | ||
|
||
The methods of this client are dynamic, and they are generated from | ||
the `methods` field of the exporter driver configuration. | ||
|
||
## Driver configuration | ||
```yaml | ||
export: | ||
example: | ||
type: jumpstarter_driver_shell.driver.Shell | ||
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" | ||
# optional parameters | ||
cwd: "/tmp" | ||
log_level: "INFO" | ||
shell: | ||
- "/bin/bash" | ||
- "-c" | ||
``` | ||
## ShellClient API | ||
Assuming the exporter driver is configured as in the example above, the client | ||
methods will be generated dynamically, and they will be available as follows: | ||
```{eval-rst} | ||
.. autoclass:: jumpstarter_driver_shell.client.ShellClient | ||
:members: | ||
|
||
.. function:: ls() | ||
:noindex: | ||
|
||
:returns: A tuple(stdout, stderr, return_code) | ||
|
||
.. function:: method2() | ||
:noindex: | ||
|
||
:returns: A tuple(stdout, stderr, return_code) | ||
|
||
.. function:: method3(arg1, arg2) | ||
:noindex: | ||
|
||
:returns: A tuple(stdout, stderr, return_code) | ||
|
||
.. function:: env_var(arg1, arg2, ENV_VAR="value") | ||
:noindex: | ||
|
||
:returns: A tuple(stdout, stderr, return_code) | ||
``` |