issue #278: ssh: support ssh_debug_level option and log TTY output.

Now debug logs may be captured all the way through the connection.
pull/282/head
David Wilson 6 years ago
parent bf478b451b
commit b58603c7a4

@ -707,7 +707,8 @@ Router Class
are already compressed, however it has a large effect on every are already compressed, however it has a large effect on every
remaining message in the otherwise uncompressed stream protocol, remaining message in the otherwise uncompressed stream protocol,
such as function call arguments and return values. such as function call arguments and return values.
:parama int ssh_debug_level:
Optional integer `0..3` indicating the SSH client debug level.
:raises mitogen.ssh.PasswordError: :raises mitogen.ssh.PasswordError:
A password was requested but none was specified, or the specified A password was requested but none was specified, or the specified
password was incorrect. password was incorrect.

@ -620,16 +620,27 @@ class TtyLogStream(mitogen.core.BasicStream):
self.receive_side = mitogen.core.Side(stream, tty_fd) self.receive_side = mitogen.core.Side(stream, tty_fd)
self.transmit_side = self.receive_side self.transmit_side = self.receive_side
self.stream = stream self.stream = stream
self.buf = ''
def __repr__(self): def __repr__(self):
return 'mitogen.parent.TtyLogStream(%r)' % (self.stream,) return 'mitogen.parent.TtyLogStream(%r)' % (self.stream.name,)
def on_receive(self, broker): def on_receive(self, broker):
"""
This handler is only called after the stream is registered with the IO
loop, the descriptor is manually read/written by _connect_bootstrap()
prior to that.
"""
buf = self.receive_side.read() buf = self.receive_side.read()
if not buf: if not buf:
return self.on_disconnect(broker) return self.on_disconnect(broker)
LOG.debug('%r.on_receive(): %r', self, buf) self.buf += buf
while '\n' in self.buf:
lines = self.buf.split('\n')
self.buf = lines[-1]
for line in lines[:-1]:
LOG.debug('%r: %r', self, line.rstrip())
class Stream(mitogen.core.Stream): class Stream(mitogen.core.Stream):

@ -43,12 +43,43 @@ import mitogen.parent
LOG = logging.getLogger('mitogen') LOG = logging.getLogger('mitogen')
PASSWORD_PROMPT = 'password:' # sshpass uses 'assword' because it doesn't lowercase the input.
PASSWORD_PROMPT = 'password'
PERMDENIED_PROMPT = 'permission denied' PERMDENIED_PROMPT = 'permission denied'
HOSTKEY_REQ_PROMPT = 'are you sure you want to continue connecting (yes/no)?' HOSTKEY_REQ_PROMPT = 'are you sure you want to continue connecting (yes/no)?'
HOSTKEY_FAIL = 'host key verification failed.' HOSTKEY_FAIL = 'host key verification failed.'
DEBUG_PREFIXES = ('debug1:', 'debug2:', 'debug3:')
def _filter_debug(stream, it, buf):
while True:
if not buf.startswith(DEBUG_PREFIXES):
return buf
while '\n' in buf:
line, _, buf = buf.partition('\n')
LOG.debug('%r: received %r', stream, line.rstrip())
try:
buf += next(it)
except StopIteration:
return buf
def filter_debug(stream, it):
"""
Read line chunks from it, either yielding them directly, or building up and
logging individual lines if they look like SSH debug output.
This contains the mess of dealing with both line-oriented input, and partial
lines such as the password prompt.
"""
for chunk in it:
chunk = _filter_debug(stream, it, chunk)
if chunk:
yield chunk
class PasswordError(mitogen.core.StreamError): class PasswordError(mitogen.core.StreamError):
pass pass
@ -62,6 +93,9 @@ class Stream(mitogen.parent.Stream):
child_is_immediate_subprocess = False child_is_immediate_subprocess = False
python_path = 'python2.7' python_path = 'python2.7'
#: Number of -v invocations to pass on command line.
ssh_debug_level = 0
#: Once connected, points to the corresponding TtyLogStream, allowing it to #: Once connected, points to the corresponding TtyLogStream, allowing it to
#: be disconnected at the same time this stream is being torn down. #: be disconnected at the same time this stream is being torn down.
tty_stream = None tty_stream = None
@ -82,7 +116,8 @@ class Stream(mitogen.parent.Stream):
def construct(self, hostname, username=None, ssh_path=None, port=None, def construct(self, hostname, username=None, ssh_path=None, port=None,
check_host_keys='enforce', password=None, identity_file=None, check_host_keys='enforce', password=None, identity_file=None,
compression=True, ssh_args=None, keepalive_enabled=True, compression=True, ssh_args=None, keepalive_enabled=True,
keepalive_count=3, keepalive_interval=15, **kwargs): keepalive_count=3, keepalive_interval=15,
ssh_debug_level=None, **kwargs):
super(Stream, self).construct(**kwargs) super(Stream, self).construct(**kwargs)
if check_host_keys not in ('accept', 'enforce', 'ignore'): if check_host_keys not in ('accept', 'enforce', 'ignore'):
raise ValueError(self.check_host_keys_msg) raise ValueError(self.check_host_keys_msg)
@ -101,6 +136,8 @@ class Stream(mitogen.parent.Stream):
self.ssh_path = ssh_path self.ssh_path = ssh_path
if ssh_args: if ssh_args:
self.ssh_args = ssh_args self.ssh_args = ssh_args
if ssh_debug_level:
self.ssh_debug_level = ssh_debug_level
def on_disconnect(self, broker): def on_disconnect(self, broker):
self.tty_stream.on_disconnect(broker) self.tty_stream.on_disconnect(broker)
@ -108,6 +145,8 @@ class Stream(mitogen.parent.Stream):
def get_boot_command(self): def get_boot_command(self):
bits = [self.ssh_path] bits = [self.ssh_path]
if self.ssh_debug_level:
bits += ['-' + ('v' * min(3, self.ssh_debug_level))]
if self.username: if self.username:
bits += ['-l', self.username] bits += ['-l', self.username]
if self.port is not None: if self.port is not None:
@ -179,9 +218,10 @@ class Stream(mitogen.parent.Stream):
deadline=self.connect_deadline deadline=self.connect_deadline
) )
for buf in it: for buf in filter_debug(self, it):
LOG.debug('%r: received %r', self, buf) LOG.debug('%r: received %r', self, buf)
if buf.endswith('EC0\n'): if buf.endswith('EC0\n'):
self._router.broker.start_receive(self.tty_stream)
self._ec0_received() self._ec0_received()
return return
elif HOSTKEY_REQ_PROMPT in buf.lower(): elif HOSTKEY_REQ_PROMPT in buf.lower():

Loading…
Cancel
Save