This repository was archived by the owner on Jan 23, 2026. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 18
Tmtee helper driver #641
Merged
mangelajo
merged 3 commits into
jumpstarter-dev:main
from
mangelajo:tmtee-helper-driver
Sep 23, 2025
Merged
Tmtee helper driver #641
Changes from all commits
Commits
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or 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 hidden or 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 @@ | ||
| ../../../../../packages/jumpstarter-driver-tmt/README.md |
This file contains hidden or 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 |
|---|---|---|
| @@ -1,10 +1,29 @@ | ||
| import inspect | ||
| import logging | ||
| from dataclasses import dataclass | ||
|
|
||
| import click | ||
| from rich import traceback | ||
|
|
||
| from jumpstarter.client import DriverClient | ||
|
|
||
|
|
||
| def _opt_log_level_callback(ctx, param, value): | ||
| traceback.install() | ||
|
|
||
| # Set the log level | ||
| log_level = value.upper() if value else "INFO" | ||
|
|
||
| # Update the root logger level to ensure all loggers inherit it | ||
| root_logger = logging.getLogger() | ||
| root_logger.setLevel(log_level) | ||
|
|
||
| # Update all existing loggers to use the new level | ||
| for logger_name in logging.Logger.manager.loggerDict: | ||
| logger = logging.getLogger(logger_name) | ||
| logger.setLevel(log_level) | ||
|
|
||
|
|
||
| @dataclass(kw_only=True) | ||
| class CompositeClient(DriverClient): | ||
| def __getattr__(self, name): | ||
|
|
@@ -17,12 +36,25 @@ def close(self): | |
|
|
||
| def cli(self): | ||
| @click.group | ||
| @click.option( | ||
| "--log-level", | ||
| "log_level", | ||
| type=click.Choice(["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"]), | ||
| help="Set the log level", | ||
| expose_value=False, | ||
| callback=_opt_log_level_callback, | ||
| ) | ||
| def base(): | ||
| """Generic composite device""" | ||
| pass | ||
|
|
||
| for k, v in self.children.items(): | ||
| if hasattr(v, "cli"): | ||
| base.add_command(v.cli(), k) | ||
| # Check if the cli method accepts a click_group parameter | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. What does click_group do?
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It's used to setup a top level command without subcommands in j |
||
| sig = inspect.signature(v.cli) | ||
| if "click_group" in sig.parameters: | ||
| base.add_command(v.cli(click_group=base), k) | ||
| else: | ||
| base.add_command(v.cli(), k) | ||
|
|
||
| return base | ||
This file contains hidden or 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 hidden or 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,109 @@ | ||
| # TMT Driver | ||
|
|
||
| `jumpstarter-driver-tmt` provides functionality for running TMT (Test Management Tool) commands locally while connecting to remote devices via SSH network connections. This driver allows you to execute TMT test plans and commands that provision and test remote hardware through SSH connections. | ||
|
|
||
| ## Installation | ||
|
|
||
| ```shell | ||
| pip3 install --extra-index-url https://pkg.jumpstarter.dev/simple/ jumpstarter-driver-tmt | ||
| ``` | ||
|
|
||
| ## Configuration | ||
|
|
||
| Example configuration: | ||
|
|
||
| ```yaml | ||
| export: | ||
| tmt: | ||
| type: jumpstarter_driver_tmt.driver.TMT | ||
| config: | ||
| reboot_cmd: "j power cycle" | ||
| default_username: "root" | ||
| default_password: "somePassword" | ||
| children: | ||
| ssh: | ||
| type: jumpstarter_driver_network.driver.TcpNetwork | ||
| config: | ||
| host: "192.168.1.100" | ||
| port: 22 | ||
| enable_address: true | ||
| ``` | ||
|
|
||
| ### Config parameters | ||
|
|
||
| | Parameter | Description | Type | Required | Default | | ||
| |-----------|-------------|------|----------|---------| | ||
| | reboot_cmd | Command to reboot the target device | str | no | "j power cycle" | | ||
| | default_username | Default username for SSH connections | str | no | "" | | ||
| | default_password | Default password for SSH connections | str | no | "" | | ||
|
|
||
| ### Required Children | ||
|
|
||
| | Child | Description | Required | | ||
| |-------|-------------|----------| | ||
| | ssh | Network TCP driver instance for SSH connection | yes | | ||
|
|
||
| ## Usage | ||
|
|
||
| ### CLI Commands | ||
|
|
||
| The TMT driver provides a CLI command `tmt` that allows you to run TMT commands locally while connecting to remote devices: | ||
|
|
||
| ```bash | ||
| # assuming that your DUT has a power and storage driver | ||
| j power on | ||
| j storage flash .... | ||
|
|
||
| # Running part of your plan with tmt | ||
| j tmt --root . -c tracing=off -c arch=aarch64 -c distro=rhel-9 -c hw_target=rcar_s4 run --workdir-root /tmp/ -a -vv provision .. some other provisioning... plan -vv --name ^/podman/plans/fusa/tests$ | ||
|
|
||
| # Use SSH port forwarding (if no direct connection to the DUT is possible) | ||
| j tmt --forward-ssh .... | ||
|
|
||
| # Specify custom username and password | ||
| j tmt --tmt-username root --tmt-password mypassword ... | ||
|
|
||
| # Raise log level of the tmt wrapper driver | ||
| j --log-level DEBUG tmt --root . -c tracing=off -c arch=aarch64 -c distro=rhel-9 -c hw_target=r | ||
| car_s4 run --workdir-root /tmp/ -a -vv provision .. some other provisioning... plan -vv --name ^/podman/plans/fusa/te | ||
| sts$ | ||
| [09/22/25 13:27:18] DEBUG Using direct SSH connection for tmt - host: 127.0.0.1, port: 2222 client.py:64 | ||
| DEBUG Provision to be replaced: ('provision', '..', 'some', 'other', client.py:117 | ||
| 'provisioning...') | ||
| DEBUG Will be replaced with: ['provision', '-h', 'connect', '-g', '127.0.0.1', client.py:118 | ||
| '-P', '2222', '-u', 'root', '-p', '******'] | ||
| DEBUG Running TMT command: ['tmt', '--root', '.', '-c', 'tracing=off', '-c', client.py:74 | ||
| 'arch=aarch64', '-c', 'distro=rhel-9', '-c', 'hw_target=rcar_s4', 'run', | ||
| '--workdir-root', '/tmp/', '-a', '-vv', 'provision', '-h', 'connect', '-g', | ||
| '127.0.0.1', '-P', '2222', '-u', 'root', '-p', '******', 'plan', '-vv', | ||
| '--name', '^/podman/plans/fusa/tests$'] | ||
|
coderabbitai[bot] marked this conversation as resolved.
|
||
| /tmp/run-018 | ||
| ... | ||
| ``` | ||
|
|
||
| ### CLI Options | ||
|
|
||
| | Option | Description | Default | | ||
| |--------|-------------|---------| | ||
| | `--forward-ssh` | Use SSH port forwarding for connection | false | | ||
| | `--tmt-username` | Username for SSH connections | from config | | ||
| | `--tmt-password` | Password for SSH connections | from config | | ||
| | `--tmt-cmd` | TMT command to execute | "tmt" | | ||
| | `--tmt-on-exporter` | Run TMT on the exporter (not implemented) | false | | ||
|
|
||
| ### Provision Arguments Handling | ||
|
|
||
| The driver automatically handles TMT provision arguments by: | ||
|
|
||
| 1. **Detecting provision sections**: Looks for `provision` or `run` commands in the TMT arguments | ||
| 2. **Replacing connection details**: Automatically replaces or adds SSH connection parameters (`-h connect -g <host> -P <port> -u <username> -p <password>`) | ||
| 3. **Preserving other arguments**: Keeps all other TMT arguments intact | ||
|
|
||
| Example of how arguments are transformed: | ||
| ```bash | ||
| # Input command | ||
| j tmt run --name /my/test/plan provision -h connect -g 192.168.1.100 -P 22 | ||
|
|
||
| # Automatically transformed to use SSH connection | ||
| # TMT receives: run --name /my/test/plan provision -h connect -g <forwarded_host> -P <forwarded_port> -u root -p password | ||
| ``` | ||
This file contains hidden or 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,27 @@ | ||
| apiVersion: jumpstarter.dev/v1alpha1 | ||
| kind: ExporterConfig | ||
| metadata: | ||
| namespace: default | ||
| name: demo | ||
| endpoint: grpc.jumpstarter.192.168.0.203.nip.io:8082 | ||
| token: "<token>" | ||
| export: | ||
| tmt: | ||
| type: jumpstarter_driver_tmt.driver.TMT | ||
| config: | ||
| reboot_cmd: "j power cycle" | ||
| default_username: "root" | ||
| default_password: "somePass" | ||
| children: | ||
| ssh: | ||
| ref: ssh | ||
| ssh: | ||
| type: jumpstarter_driver_network.driver.TcpNetwork | ||
| config: | ||
| host: 127.0.0.1 | ||
| port: 2222 | ||
| enable_address: true | ||
| power: | ||
| type: jumpstarter_driver_power.driver.MockPower | ||
|
|
||
|
|
Empty file.
157 changes: 157 additions & 0 deletions
157
packages/jumpstarter-driver-tmt/jumpstarter_driver_tmt/client.py
This file contains hidden or 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,157 @@ | ||
| import subprocess | ||
| from dataclasses import dataclass | ||
| from urllib.parse import urlparse | ||
|
|
||
| import click | ||
| from jumpstarter_driver_composite.client import CompositeClient | ||
| from jumpstarter_driver_network.adapters import TcpPortforwardAdapter | ||
|
|
||
| from jumpstarter.client.core import DriverMethodNotImplemented | ||
|
|
||
|
|
||
| def redact_password_in_args(args): | ||
| """Redact password arguments in a list for safe logging""" | ||
| redacted = list(args) | ||
| try: | ||
| # Find -p flag and redact the next argument (password) | ||
| p_index = redacted.index("-p") | ||
| if p_index + 1 < len(redacted): | ||
| redacted[p_index + 1] = "******" | ||
| except ValueError: | ||
| # -p flag not found, nothing to redact | ||
| pass | ||
| return redacted | ||
|
|
||
|
|
||
| @dataclass(kw_only=True) | ||
| class TMTClient(CompositeClient): | ||
| """ | ||
| Client interface for LocalTMT driver | ||
|
|
||
| This client provides methods to interact with LocalTMT devices via SSH | ||
| """ | ||
|
|
||
| def cli(self, click_group): | ||
|
|
||
| @click_group.command(context_settings={"ignore_unknown_options": True}) | ||
| @click.option("--forward-ssh", is_flag=True) | ||
| @click.option("--tmt-username", default=None) | ||
| @click.option("--tmt-password", default=None) | ||
| @click.option("--tmt-cmd", default="tmt") | ||
| @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") | ||
| else: | ||
| result = self.run_tmt_local(forward_ssh, tmt_cmd, tmt_username, tmt_password, args) | ||
| self.logger.debug(f"TMT result: {result}") | ||
| if result != 0: | ||
| click.get_current_context().exit(result) | ||
| return result | ||
|
|
||
| return tmt | ||
|
mangelajo marked this conversation as resolved.
|
||
|
|
||
| def run_tmt_local(self, forward_ssh, tmt_cmd, username, password, args): | ||
| # if we are asked to forward the ssh connection, or we have to fallback, we do that | ||
| def_user, def_pass = self.call("get_default_user_pass") | ||
| username = username or def_user | ||
| password = password or def_pass | ||
| hard_reboot_cmd = self.call("get_reboot_cmd") | ||
| if forward_ssh: | ||
| self.logger.debug("Using SSH port forwarding for TMT connection") | ||
| with TcpPortforwardAdapter( | ||
| client=self.ssh, | ||
| ) as addr: | ||
| host = addr[0] | ||
| port = addr[1] | ||
| self.logger.debug(f"SSH port forward established - host: {host}, port: {port}") | ||
| return self._run_tmt_local(host, port, tmt_cmd, username, password, hard_reboot_cmd, args) | ||
|
mangelajo marked this conversation as resolved.
|
||
| else: | ||
| # if we are not asked to forward the ssh connection, we try to get the address from the ssh driver | ||
| try: | ||
| address = self.ssh.address() # (format: "tcp://host:port") | ||
| parsed = urlparse(address) | ||
| host = parsed.hostname | ||
| port = parsed.port | ||
| if not host or not port: | ||
| raise ValueError(f"Invalid address format: {address}") | ||
| self.logger.debug(f"Using direct SSH connection for tmt - host: {host}, port: {port}") | ||
| return self._run_tmt_local(host, port, tmt_cmd, username, password, hard_reboot_cmd, args) | ||
| except (DriverMethodNotImplemented, ValueError) as e: | ||
| self.logger.warning(f"Direct address connection failed ({e}), falling back to SSH port forwarding") | ||
| return self.run_tmt_local(True, tmt_cmd, username, password, args) | ||
|
|
||
| def _run_tmt_local(self, host, port, tmt_cmd, username, password, hard_reboot_cmd, args): | ||
| """Run TMT command with the given host, port, and arguments""" | ||
| # This is a placeholder implementation - replace with actual TMT command execution | ||
| args = replace_provision_args(self.logger, args, host, port, username, password, hard_reboot_cmd) | ||
| # Redact password for safe logging | ||
| safe_args = redact_password_in_args(args) | ||
| self.logger.debug(f"Running TMT command: {[tmt_cmd] + safe_args}") | ||
| # execute the command on the local machine | ||
| try: | ||
| result = subprocess.run([tmt_cmd] + args) | ||
| return result.returncode | ||
| except FileNotFoundError: | ||
| self.logger.error( | ||
| f"TMT command '{tmt_cmd}' not found. Please ensure TMT is installed and available in PATH." | ||
| ) | ||
| return 127 # Standard exit code for "command not found" | ||
|
|
||
| # the tmt commands are executed locally, but we need to identify | ||
| # the connection part of the commandline and replace it or insert our own | ||
| # this is a possible set of args that we may receive: | ||
| # --root . -c tracing=off -c arch=aarch64 -c distro=rhel-9 -c hw_target=rcar_s4 run | ||
| # --workdir-root /tmp/ -a -vv provision -h connect -g $IP -P $PORT -u root -p password --help plan -vv | ||
| # --name ^/podman/plans/fusa/tests$ | ||
| # | ||
| # in this case we need to identify the provision section and replace it with our own, | ||
| # and reuse the -u root -p password if provided | ||
| # provision can have any number of -flag arguments, so we need to identify them and replace them with our own | ||
| def replace_provision_args(logger, args, host, port, username, password, hard_reboot_cmd): | ||
| """Replace or add provision arguments for TMT command""" | ||
| # this list is used to identify the end of the provision section | ||
| TMT_RUN_CMDS = [ | ||
| "discover", "provision", "prepare", "execute", "report", "finish", "cleanup", | ||
| "login", "reboot", "plan", "plans", "test", "run" | ||
| ] | ||
| # find the provision section | ||
| provision_args = ["provision","-h", "connect", "-g", host, "-P", str(port)] | ||
| if username: | ||
| provision_args.append("-u") | ||
| provision_args.append(username) | ||
| if password: | ||
| provision_args.append("-p") | ||
| provision_args.append(password) | ||
| if hard_reboot_cmd: | ||
| provision_args.append("--hard-reboot") | ||
| provision_args.append(hard_reboot_cmd) | ||
| try: | ||
| provision_index = args.index("provision") | ||
| except ValueError: | ||
| # "provision" not found in args | ||
| if "run" in args: | ||
| logger.debug("Run section found, adding provision arguments") | ||
| return list(args) + provision_args | ||
| else: | ||
| logger.debug("Provision or run section not found, ignoring") | ||
| return list(args) | ||
|
|
||
| next_cmd_index = provision_index + 1 | ||
| while next_cmd_index < len(args) and args[next_cmd_index] not in TMT_RUN_CMDS: | ||
| next_cmd_index += 1 | ||
| # Redact passwords for safe logging | ||
| safe_provision_section = redact_password_in_args(args[provision_index:next_cmd_index]) | ||
| safe_provision_args = redact_password_in_args(provision_args) | ||
| logger.debug(f"Provision to be replaced: {safe_provision_section}") | ||
| logger.debug(f"Will be replaced with: {safe_provision_args}") | ||
| # get the provision section | ||
| before_provision = args[:provision_index] | ||
| after_provision = args[next_cmd_index:] | ||
|
|
||
| args = list(before_provision) + provision_args + list(after_provision) | ||
| return args | ||
|
|
||
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why can't we just import this from
jumpstarter_cli_common?