|
| 1 | +import logging |
| 2 | +import os |
| 3 | +import subprocess |
| 4 | +from dataclasses import dataclass, field |
| 5 | + |
| 6 | +from jumpstarter.driver import Driver, export |
| 7 | + |
| 8 | +logger = logging.getLogger(__name__) |
| 9 | + |
| 10 | +@dataclass(kw_only=True) |
| 11 | +class Shell(Driver): |
| 12 | + """shell driver for Jumpstarter""" |
| 13 | + |
| 14 | + # methods field is used to define the methods exported, and the shell script |
| 15 | + # to be executed by each method |
| 16 | + methods: dict[str, str] |
| 17 | + shell: list[str] = field(default_factory=lambda: ["bash", "-c"]) |
| 18 | + log_level: str = "INFO" |
| 19 | + |
| 20 | + def __post_init__(self): |
| 21 | + super().__post_init__() |
| 22 | + # set logger log level |
| 23 | + logger.setLevel(self.log_level) |
| 24 | + |
| 25 | + @classmethod |
| 26 | + def client(cls) -> str: |
| 27 | + return "jumpstarter_driver_shell.client.ShellClient" |
| 28 | + |
| 29 | + @export |
| 30 | + def get_methods(self) -> list[str]: |
| 31 | + methods = list(self.methods.keys()) |
| 32 | + logger.debug(f"get_methods called, returning methods: {methods}") |
| 33 | + return methods |
| 34 | + |
| 35 | + @export |
| 36 | + def call_method(self, method: str, env, *args): |
| 37 | + logger.info(f"calling {method} with args: {args} and kwargs as env: {env}") |
| 38 | + script = self.methods[method] |
| 39 | + logger.debug(f"running script: {script}") |
| 40 | + result = self._run_inline_shell_script(method, script, *args, env_vars=env) |
| 41 | + if result.returncode != 0: |
| 42 | + logger.info(f"{method} return code: {result.returncode}") |
| 43 | + if result.stderr != "": |
| 44 | + logger.debug(f"{method} stderr:\n{result.stderr.rstrip("\n")}") |
| 45 | + if result.stdout != "": |
| 46 | + logger.debug(f"{method} stdout:\n{result.stdout.rstrip("\n")}") |
| 47 | + return result.stdout, result.stderr, result.returncode |
| 48 | + |
| 49 | + def _run_inline_shell_script(self, method, script, *args, env_vars=None): |
| 50 | + """ |
| 51 | + Run the given shell script (as a string) with optional arguments and |
| 52 | + environment variables. Returns a CompletedProcess with stdout, stderr, and returncode. |
| 53 | +
|
| 54 | + :param script: The shell script contents as a string. |
| 55 | + :param args: Arguments to pass to the script (mapped to $1, $2, etc. in the script). |
| 56 | + :param env_vars: A dict of environment variables to make available to the script. |
| 57 | +
|
| 58 | + :return: A subprocess.CompletedProcess object (Python 3.5+). |
| 59 | + """ |
| 60 | + |
| 61 | + # Merge parent environment with the user-supplied env_vars |
| 62 | + # so that we don't lose existing environment variables. |
| 63 | + combined_env = os.environ.copy() |
| 64 | + if env_vars: |
| 65 | + combined_env.update(env_vars) |
| 66 | + |
| 67 | + cmd = self.shell + [script, method] + list(args) |
| 68 | + |
| 69 | + # Run the command |
| 70 | + result = subprocess.run( |
| 71 | + cmd, |
| 72 | + capture_output=True, # Captures stdout and stderr |
| 73 | + text=True, # Returns stdout/stderr as strings (not bytes) |
| 74 | + env=combined_env # Pass our merged environment |
| 75 | + ) |
| 76 | + |
| 77 | + return result |
0 commit comments