Skip to content
This repository was archived by the owner on Jan 23, 2026. It is now read-only.

Commit 7069788

Browse files
committed
feat(driver-ssh): add --user selection to j ssh
Allow `j ssh -u someuser` to be able to select the user with which to connect to the board. Signed-off-by: Albert Esteve <aesteve@redhat.com>
1 parent 5300757 commit 7069788

2 files changed

Lines changed: 42 additions & 9 deletions

File tree

packages/jumpstarter-driver-ssh/jumpstarter_driver_ssh/client.py

Lines changed: 10 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -62,16 +62,17 @@ def cli(self):
6262
help="Run SSH command with arguments",
6363
)
6464
@click.option("--direct", is_flag=True, help="Use direct TCP address")
65+
@click.option("-u", "--user", help="Username to use for SSH connection")
6566
@click.argument("args", nargs=-1)
66-
def ssh(direct, args):
67+
def ssh(direct, user, args):
6768
options = SSHCommandRunOptions(
6869
direct=direct,
6970
# For the CLI, we never capture output so that interactive shells
7071
# and long-running commands stream their output directly.
7172
capture_output=False,
7273
)
7374

74-
result = self.run(options, args)
75+
result = self.run(options, args, user=user)
7576
self.logger.debug("SSH exit code: %s", result.return_code)
7677

7778
if result.stdout:
@@ -118,7 +119,7 @@ def username(self) -> str:
118119
"""Get the default SSH username"""
119120
return self.call("get_default_username")
120121

121-
def run(self, options: SSHCommandRunOptions, args) -> SSHCommandRunResult:
122+
def run(self, options: SSHCommandRunOptions, args, user: str | None = None) -> SSHCommandRunResult:
122123
"""Run SSH command with the given parameters and arguments"""
123124
# Get SSH command and default username from driver
124125
if options.direct:
@@ -131,7 +132,7 @@ def run(self, options: SSHCommandRunOptions, args) -> SSHCommandRunResult:
131132
if not host or not port:
132133
raise ValueError(f"Invalid address format: {address}")
133134
self.logger.debug("Using direct TCP connection for SSH - host: %s, port: %s", host, port)
134-
return self._run_ssh_local(host, port, options, args)
135+
return self._run_ssh_local(host, port, options, args, user)
135136
except (DriverMethodNotImplemented, ValueError) as e:
136137
self.logger.error("Direct address connection failed (%s), falling back to SSH port forwarding", e)
137138
return self.run(SSHCommandRunOptions(
@@ -147,9 +148,9 @@ def run(self, options: SSHCommandRunOptions, args) -> SSHCommandRunResult:
147148
) as addr:
148149
host, port = addr
149150
self.logger.debug("SSH port forward established - host: %s, port: %s", host, port)
150-
return self._run_ssh_local(host, port, options, args)
151+
return self._run_ssh_local(host, port, options, args, user)
151152

152-
def _run_ssh_local(self, host, port, options, args):
153+
def _run_ssh_local(self, host, port, options, args, user: str | None = None):
153154
"""Run SSH command with the given host, port, and arguments"""
154155
# Create temporary identity file if needed
155156
ssh_identity = self.identity
@@ -175,7 +176,7 @@ def _run_ssh_local(self, host, port, options, args):
175176

176177
try:
177178
# Build SSH command arguments
178-
ssh_args = self._build_ssh_command_args(port, identity_file, args)
179+
ssh_args = self._build_ssh_command_args(port, identity_file, args, user)
179180

180181
# Separate SSH options from command arguments
181182
ssh_options, command_args = self._separate_ssh_options_and_command_args(args)
@@ -194,11 +195,11 @@ def _run_ssh_local(self, host, port, options, args):
194195
except Exception as e:
195196
self.logger.warning("Failed to clean up temporary identity file %s: %s", identity_file, str(e))
196197

197-
def _build_ssh_command_args(self, port, identity_file, args):
198+
def _build_ssh_command_args(self, port, identity_file, args, user: str | None = None):
198199
"""Build initial SSH command arguments"""
199200
# Split the SSH command into individual arguments
200201
ssh_args = shlex.split(self.command)
201-
default_username = self.username
202+
default_username = user or self.username
202203

203204
# Add identity file if provided
204205
if identity_file:

packages/jumpstarter-driver-ssh/jumpstarter_driver_ssh/driver_test.py

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -105,6 +105,38 @@ def test_ssh_command_without_default_username():
105105
assert result.stdout == "some stdout"
106106

107107

108+
def test_ssh_command_with_explicit_user_parameter():
109+
"""Test SSH command execution with the user parameter overriding the default."""
110+
instance = SSHWrapper(
111+
children={"tcp": TcpNetwork(host="127.0.0.1", port=22)},
112+
default_username="testuser",
113+
)
114+
115+
with serve(instance) as client:
116+
with patch("subprocess.run") as mock_run:
117+
mock_run.return_value = MagicMock(returncode=0, stdout="some stdout", stderr="")
118+
119+
# Call run with an explicit user.
120+
result = client.run(SSHCommandRunOptions(direct=False), ["hostname"], user="overrideuser")
121+
assert isinstance(result, SSHCommandRunResult)
122+
123+
# Verify subprocess.run was called.
124+
assert mock_run.called
125+
call_args = mock_run.call_args[0][0]
126+
127+
# Check that the override user is present.
128+
assert "-l" in call_args
129+
assert "overrideuser" in call_args
130+
assert "testuser" not in call_args
131+
assert call_args[call_args.index("-l") + 1] == "overrideuser"
132+
133+
assert "127.0.0.1" in call_args
134+
assert "hostname" in call_args
135+
136+
assert result.return_code == 0
137+
assert result.stdout == "some stdout"
138+
139+
108140
def test_ssh_command_with_user_override():
109141
"""Test SSH command execution with -l flag overriding default username"""
110142
instance = SSHWrapper(

0 commit comments

Comments
 (0)