Check for errors on remote commands.

This commit is contained in:
Tommi Virtanen 2011-05-19 13:05:14 -07:00
parent 87d7192c89
commit df84f4e052
2 changed files with 196 additions and 5 deletions

View File

@ -8,8 +8,9 @@ import shutil
log = logging.getLogger(__name__)
class RemoteProcess(object):
__slots__ = ['stdin', 'stdout', 'stderr', '_get_exitstatus']
def __init__(self, stdin, stdout, stderr, get_exitstatus):
__slots__ = ['command', 'stdin', 'stdout', 'stderr', '_get_exitstatus']
def __init__(self, command, stdin, stdout, stderr, get_exitstatus):
self.command = command
self.stdin = stdin
self.stdout = stdout
self.stderr = stderr
@ -44,6 +45,7 @@ def execute(client, args):
cmd = ' '.join(pipes.quote(a) for a in args)
(in_, out, err) = client.exec_command(cmd)
r = RemoteProcess(
command=cmd,
stdin=in_,
stdout=out,
stderr=err,
@ -74,10 +76,43 @@ def copy_file_to(f, dst):
handler = shutil.copyfileobj
return handler(f, dst)
class CommandFailedError(Exception):
def __init__(self, command, exitstatus):
self.command = command
self.exitstatus = exitstatus
def __str__(self):
return "Command failed with status {status}: {command!r}".format(
status=self.exitstatus,
command=self.command,
)
class CommandCrashedError(Exception):
def __init__(self, command):
self.command = command
def __str__(self):
return "Command crashed: {command!r}".format(
command=self.command,
)
class ConnectionLostError(Exception):
def __init__(self, command):
self.command = command
def __str__(self):
return "SSH connection was lost: {command!r}".format(
command=self.command,
)
def run(
client, args,
stdin=None, stdout=None, stderr=None,
logger=None,
check_status=True,
):
"""
Run a command remotely.
@ -89,6 +124,7 @@ def run(
:param stdout: What to do with standard output. Either a file-like object, a `logging.Logger`, or `None` for copying to default log.
:param stderr: What to do with standard error. See `stdout`.
:param logger: If logging, write stdout/stderr to "out" and "err" children of this logger. Defaults to logger named after this module.
:param check_status: Whether to raise CalledProcessError on non-zero exit status, and . Defaults to True. All signals and connection loss are made to look like SIGHUP.
"""
r = execute(client, args)
@ -108,4 +144,19 @@ def run(
g_err.get()
g_in.get()
return r.exitstatus
status = r.exitstatus
if check_status:
if status is None:
# command either died due to a signal, or the connection
# was lost
transport = client.get_transport()
if not transport.is_active():
# look like we lost the connection
raise ConnectionLostError(command=r.command)
# connection seems healthy still, assuming it was a
# signal; sadly SSH does not tell us which signal
raise CommandCrashedError(command=r.command)
if status != 0:
raise CommandFailedError(command=r.command, exitstatus=status)
return status

View File

@ -6,6 +6,8 @@ import logging
from .. import run
from .util import assert_raises
@nose.with_setup(fudge.clear_expectations)
@fudge.with_fakes
@ -30,18 +32,100 @@ def test_run_log_simple():
log_out.expects('log').with_args(logging.INFO, 'bar')
channel = fudge.Fake('channel')
out.has_attr(channel=channel)
channel.expects('recv_exit_status').with_args().returns(42)
channel.expects('recv_exit_status').with_args().returns(0)
got = run.run(
client=ssh,
logger=logger,
args=['foo', 'bar baz'],
)
eq(got, 0)
@nose.with_setup(fudge.clear_expectations)
@fudge.with_fakes
def test_run_status_bad():
ssh = fudge.Fake('SSHConnection')
cmd = ssh.expects('exec_command')
cmd.with_args("foo")
in_ = fudge.Fake('ChannelFile').is_a_stub()
out = fudge.Fake('ChannelFile').is_a_stub()
err = fudge.Fake('ChannelFile').is_a_stub()
cmd.returns((in_, out, err))
out.expects('xreadlines').with_args().returns([])
err.expects('xreadlines').with_args().returns([])
logger = fudge.Fake('logger').is_a_stub()
channel = fudge.Fake('channel')
out.has_attr(channel=channel)
channel.expects('recv_exit_status').with_args().returns(42)
e = assert_raises(
run.CommandFailedError,
run.run,
client=ssh,
logger=logger,
args=['foo'],
)
eq(e.command, 'foo')
eq(e.exitstatus, 42)
eq(str(e), "Command failed with status 42: 'foo'")
@nose.with_setup(fudge.clear_expectations)
@fudge.with_fakes
def test_run_status_bad_nocheck():
ssh = fudge.Fake('SSHConnection')
cmd = ssh.expects('exec_command')
cmd.with_args("foo")
in_ = fudge.Fake('ChannelFile').is_a_stub()
out = fudge.Fake('ChannelFile').is_a_stub()
err = fudge.Fake('ChannelFile').is_a_stub()
cmd.returns((in_, out, err))
out.expects('xreadlines').with_args().returns([])
err.expects('xreadlines').with_args().returns([])
logger = fudge.Fake('logger').is_a_stub()
channel = fudge.Fake('channel')
out.has_attr(channel=channel)
channel.expects('recv_exit_status').with_args().returns(42)
got = run.run(
client=ssh,
logger=logger,
args=['foo'],
check_status=False,
)
eq(got, 42)
@nose.with_setup(fudge.clear_expectations)
@fudge.with_fakes
def test_run_crash_status():
def test_run_status_crash():
ssh = fudge.Fake('SSHConnection')
cmd = ssh.expects('exec_command')
cmd.with_args("foo")
in_ = fudge.Fake('ChannelFile').is_a_stub()
out = fudge.Fake('ChannelFile').is_a_stub()
err = fudge.Fake('ChannelFile').is_a_stub()
cmd.returns((in_, out, err))
out.expects('xreadlines').with_args().returns([])
err.expects('xreadlines').with_args().returns([])
logger = fudge.Fake('logger').is_a_stub()
channel = fudge.Fake('channel')
out.has_attr(channel=channel)
channel.expects('recv_exit_status').with_args().returns(-1)
transport = ssh.expects('get_transport').with_args().returns_fake()
transport.expects('is_active').with_args().returns(True)
e = assert_raises(
run.CommandCrashedError,
run.run,
client=ssh,
logger=logger,
args=['foo'],
)
eq(e.command, 'foo')
eq(str(e), "Command crashed: 'foo'")
@nose.with_setup(fudge.clear_expectations)
@fudge.with_fakes
def test_run_status_crash_nocheck():
ssh = fudge.Fake('SSHConnection')
cmd = ssh.expects('exec_command')
cmd.with_args("foo")
@ -59,5 +143,61 @@ def test_run_crash_status():
client=ssh,
logger=logger,
args=['foo'],
check_status=False,
)
assert got is None
@nose.with_setup(fudge.clear_expectations)
@fudge.with_fakes
def test_run_status_lost():
ssh = fudge.Fake('SSHConnection')
cmd = ssh.expects('exec_command')
cmd.with_args("foo")
in_ = fudge.Fake('ChannelFile').is_a_stub()
out = fudge.Fake('ChannelFile').is_a_stub()
err = fudge.Fake('ChannelFile').is_a_stub()
cmd.returns((in_, out, err))
out.expects('xreadlines').with_args().returns([])
err.expects('xreadlines').with_args().returns([])
logger = fudge.Fake('logger').is_a_stub()
channel = fudge.Fake('channel')
out.has_attr(channel=channel)
channel.expects('recv_exit_status').with_args().returns(-1)
transport = ssh.expects('get_transport').with_args().returns_fake()
transport.expects('is_active').with_args().returns(False)
e = assert_raises(
run.ConnectionLostError,
run.run,
client=ssh,
logger=logger,
args=['foo'],
)
eq(e.command, 'foo')
eq(str(e), "SSH connection was lost: 'foo'")
@nose.with_setup(fudge.clear_expectations)
@fudge.with_fakes
def test_run_status_lost_nocheck():
ssh = fudge.Fake('SSHConnection')
cmd = ssh.expects('exec_command')
cmd.with_args("foo")
in_ = fudge.Fake('ChannelFile').is_a_stub()
out = fudge.Fake('ChannelFile').is_a_stub()
err = fudge.Fake('ChannelFile').is_a_stub()
cmd.returns((in_, out, err))
out.expects('xreadlines').with_args().returns([])
err.expects('xreadlines').with_args().returns([])
logger = fudge.Fake('logger').is_a_stub()
channel = fudge.Fake('channel')
out.has_attr(channel=channel)
channel.expects('recv_exit_status').with_args().returns(-1)
got = run.run(
client=ssh,
logger=logger,
args=['foo'],
check_status=False,
)
assert got is None