Skip to content

Commit 227226d

Browse files
committed
driver/sshdriver: extend with su handling
Add two new attributes to the driver which will use su to switch to a user to run a command. The su_password is required for this feature to be used, su_username only needs to be set if another user than root should be switched to. Signed-off-by: Rouven Czerwinski <[email protected]> Co-developed-by: Jan Luebbe <[email protected]>
1 parent 32bc748 commit 227226d

File tree

4 files changed

+101
-3
lines changed

4 files changed

+101
-3
lines changed

doc/configuration.rst

+7
Original file line numberDiff line numberDiff line change
@@ -1540,6 +1540,13 @@ Arguments:
15401540
target.
15411541
- explicit_sftp_mode (bool, default=False): if set to True, `put()` and `get()` will
15421542
explicitly use the SFTP protocol for file transfers instead of scp's default protocol
1543+
- su_username(str, default="root"): only used if su_password is set
1544+
- su_prompt(str, default="Passowrd:"): prompt string for su
1545+
- su_password(str, default=None): su password for the user set via su_username
1546+
1547+
.. note::
1548+
Using the su support will automatically enable stderr_merge, since ssh this
1549+
is required to interact with the password prompt.
15431550

15441551
UBootDriver
15451552
~~~~~~~~~~~

examples/ssh-su-example/conf.yaml

+11
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
targets:
2+
main:
3+
resources:
4+
NetworkService:
5+
address: 127.0.0.1
6+
username: <login_username>
7+
drivers:
8+
SSHDriver:
9+
su_password: <the_password>
10+
su_username: <the_username>
11+
stderr_merge: true

examples/ssh-su-example/test.py

+14
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
import logging
2+
3+
from labgrid import Environment
4+
5+
logging.basicConfig(
6+
level=logging.DEBUG
7+
)
8+
9+
env = Environment("conf.yaml")
10+
target = env.get_target()
11+
ssh = target.get_driver("SSHDriver")
12+
out, _, code = ssh.run("ps -p $PPID")
13+
print(code)
14+
print(out)

labgrid/driver/sshdriver.py

+69-3
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@
1111
import time
1212

1313
import attr
14+
from pexpect.fdpexpect import fdspawn
15+
from pexpect.exceptions import EOF, TIMEOUT
1416

1517
from ..factory import target_factory
1618
from ..protocol import CommandProtocol, FileTransferProtocol
@@ -22,6 +24,7 @@
2224
from ..util.proxy import proxymanager
2325
from ..util.timeout import Timeout
2426
from ..util.ssh import get_ssh_connect_timeout
27+
from ..util.marker import gen_marker
2528

2629

2730
@target_factory.reg_driver
@@ -34,6 +37,9 @@ class SSHDriver(CommandMixin, Driver, CommandProtocol, FileTransferProtocol):
3437
stderr_merge = attr.ib(default=False, validator=attr.validators.instance_of(bool))
3538
connection_timeout = attr.ib(default=float(get_ssh_connect_timeout()), validator=attr.validators.instance_of(float))
3639
explicit_sftp_mode = attr.ib(default=False, validator=attr.validators.instance_of(bool))
40+
su_password = attr.ib(default=None, validator=attr.validators.optional(attr.validators.instance_of(str)))
41+
su_username = attr.ib(default="root", validator=attr.validators.instance_of(str))
42+
su_prompt = attr.ib(default="Password:", validator=attr.validators.instance_of(str))
3743

3844
def __attrs_post_init__(self):
3945
super().__attrs_post_init__()
@@ -180,6 +186,40 @@ def _start_own_master_once(self, timeout):
180186
def run(self, cmd, codec="utf-8", decodeerrors="strict", timeout=None):
181187
return self._run(cmd, codec=codec, decodeerrors=decodeerrors, timeout=timeout)
182188

189+
def handle_password(self, fd, stdin, marker):
190+
p = fdspawn(fd, timeout=15)
191+
try:
192+
p.expect([f"{marker}\n"])
193+
except TIMEOUT:
194+
raise ExecutionError(f"Failed to find marker before su: {p.buffer!r}")
195+
except EOF:
196+
raise ExecutionError("Unexpected disconnect before su")
197+
198+
try:
199+
index = p.expect([f"{marker}\n", self.su_prompt])
200+
except TIMEOUT:
201+
raise ExecutionError(f"Unexpected output from su: {p.buffer!r}")
202+
except EOF:
203+
raise ExecutionError("Unexpected disconnect after starting su")
204+
205+
if index == 0:
206+
# no password needed
207+
return p.after
208+
209+
stdin.write(f"{self.su_password}".encode("utf-8"))
210+
# It seems we need to close stdin here to reliably get su to accept the
211+
# password. \n doesn't seem to work.
212+
stdin.close()
213+
214+
try:
215+
p.expect([f"{marker}\n"])
216+
except TIMEOUT:
217+
raise ExecutionError(f"Unexpected output from su after entering password: {p.buffer!r}")
218+
except EOF:
219+
raise ExecutionError(f"Unexpected disconnect after after entering su password: {p.before!r}")
220+
221+
return p.after
222+
183223
def _run(self, cmd, codec="utf-8", decodeerrors="strict", timeout=None):
184224
"""Execute `cmd` on the target.
185225
@@ -196,22 +236,48 @@ def _run(self, cmd, codec="utf-8", decodeerrors="strict", timeout=None):
196236
complete_cmd = ["ssh", "-x", *self.ssh_prefix,
197237
"-p", str(self.networkservice.port), "-l", self.networkservice.username,
198238
self.networkservice.address
199-
] + cmd.split(" ")
239+
]
240+
if self.su_password:
241+
self.stderr_merge = True # with -tt, we get all output on stdout
242+
marker = gen_marker()
243+
complete_cmd += ["-tt", "--", "echo", f"{marker};", "su", self.su_username, "--"]
244+
inner_cmd = f"echo '{marker[:4]}''{marker[4:]}'; {cmd}"
245+
complete_cmd += ["-c", shlex.quote(inner_cmd)]
246+
else:
247+
complete_cmd += ["--"] + cmd.split(" ")
248+
200249
self.logger.debug("Sending command: %s", complete_cmd)
201250
if self.stderr_merge:
202251
stderr_pipe = subprocess.STDOUT
203252
else:
204253
stderr_pipe = subprocess.PIPE
254+
stdin = subprocess.PIPE if self.su_password else None
255+
stdout, stderr = b"", b""
205256
try:
206257
sub = subprocess.Popen(
207-
complete_cmd, stdout=subprocess.PIPE, stderr=stderr_pipe
258+
complete_cmd, stdout=subprocess.PIPE, stderr=stderr_pipe, stdin=stdin,
208259
)
209260
except:
210261
raise ExecutionError(
211262
f"error executing command: {complete_cmd}"
212263
)
213264

214-
stdout, stderr = sub.communicate(timeout=timeout)
265+
if self.su_password:
266+
fd = sub.stdout if self.stderr_merge else sub.stderr
267+
output = self.handle_password(fd, sub.stdin, marker)
268+
sub.stdin.close()
269+
self.logger.debug(f"su leftover output: %s", output)
270+
if self.stderr_merge:
271+
stderr += output
272+
else:
273+
stdout += output
274+
275+
sub.stdin = None # never try to write to stdin here
276+
comout, comerr = sub.communicate(timeout=timeout)
277+
stdout += comout
278+
if not self.stderr_merge:
279+
stderr += comerr
280+
215281
stdout = stdout.decode(codec, decodeerrors).split('\n')
216282
if stdout[-1] == '':
217283
stdout.pop()

0 commit comments

Comments
 (0)