ceph/orchestra/run.py

163 lines
4.7 KiB
Python
Raw Normal View History

from cStringIO import StringIO
2011-05-18 21:46:49 +00:00
import gevent
import pipes
import logging
import shutil
log = logging.getLogger(__name__)
class RemoteProcess(object):
2011-05-19 20:05:14 +00:00
__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
self._get_exitstatus = get_exitstatus
@property
def exitstatus(self):
"""
Wait for exit and return exit status.
Will return None on signals and connection loss.
This will likely block until you've closed stdin and consumed
stdout and stderr.
"""
status = self._get_exitstatus()
# -1 on connection loss *and* signals; map to more pythonic None
if status == -1:
status = None
return status
2011-05-18 21:46:49 +00:00
def execute(client, args):
"""
Execute a command remotely.
2011-05-18 21:46:49 +00:00
Caller needs to handle stdin etc.
:param client: SSHConnection to run the command with
:param args: command to run
:type args: list of string
"""
cmd = ' '.join(pipes.quote(a) for a in args)
(in_, out, err) = client.exec_command(cmd)
r = RemoteProcess(
2011-05-19 20:05:14 +00:00
command=cmd,
stdin=in_,
stdout=out,
stderr=err,
get_exitstatus=out.channel.recv_exit_status,
)
return r
def copy_to_log(f, logger, loglevel=logging.INFO):
2011-05-18 21:46:49 +00:00
# i can't seem to get fudge to fake an iterable, so using this old
# api for now
for line in f.xreadlines():
line = line.rstrip()
logger.log(loglevel, line)
2011-05-18 21:46:49 +00:00
def copy_and_close(src, fdst):
if src is not None:
if isinstance(src, basestring):
src = StringIO(src)
shutil.copyfileobj(src, fdst)
fdst.close()
def copy_file_to(f, dst):
if hasattr(dst, 'log'):
# looks like a Logger to me; not using isinstance to make life
# easier for unit tests
handler = copy_to_log
else:
handler = shutil.copyfileobj
return handler(f, dst)
2011-05-19 20:05:14 +00:00
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,
2011-05-19 20:05:14 +00:00
check_status=True,
):
"""
Run a command remotely.
:param client: SSHConnection to run the command with
:param args: command to run
:type args: list of string
:param stdin: Standard input to send; either a string, a file-like object, or None.
: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.
2011-05-19 20:05:14 +00:00
: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)
g_in = gevent.spawn(copy_and_close, stdin, r.stdin)
if logger is None:
logger = log
if stderr is None:
stderr = logger.getChild('err')
g_err = gevent.spawn(copy_file_to, r.stderr, stderr)
if stdout is None:
stdout = logger.getChild('out')
copy_file_to(r.stdout, stdout)
2011-05-18 21:46:49 +00:00
g_err.get()
g_in.get()
2011-05-19 20:05:14 +00:00
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