ssh: Learn to type passwords and supply pubkeys.
Now ssh requires a tty allocation. This presents a scalability problem, a future version could selectively allocate a tty only if typing passwords is desired. Sudo's tty handling is now moved into mitogen.master.
This commit is contained in:
parent
fa427d83c8
commit
dc446f9042
|
@ -12,11 +12,13 @@ import itertools
|
|||
import logging
|
||||
import os
|
||||
import pkgutil
|
||||
import pty
|
||||
import re
|
||||
import select
|
||||
import signal
|
||||
import socket
|
||||
import sys
|
||||
import termios
|
||||
import textwrap
|
||||
import time
|
||||
import types
|
||||
|
@ -73,6 +75,78 @@ def create_child(*args):
|
|||
return pid, os.dup(parentfp.fileno())
|
||||
|
||||
|
||||
def flags(names):
|
||||
"""Return the result of ORing a set of (space separated) :py:mod:`termios`
|
||||
module constants together."""
|
||||
return sum(getattr(termios, name) for name in names.split())
|
||||
|
||||
|
||||
def cfmakeraw((iflag, oflag, cflag, lflag, ispeed, ospeed, cc)):
|
||||
"""Given a list returned by :py:func:`termios.tcgetattr`, return a list
|
||||
that has been modified in the same manner as the `cfmakeraw()` C library
|
||||
function."""
|
||||
iflag &= ~flags('IGNBRK BRKINT PARMRK ISTRIP INLCR IGNCR ICRNL IXON')
|
||||
oflag &= ~flags('OPOST IXOFF')
|
||||
lflag &= ~flags('ECHO ECHOE ECHONL ICANON ISIG IEXTEN')
|
||||
cflag &= ~flags('CSIZE PARENB')
|
||||
cflag |= flags('CS8')
|
||||
|
||||
iflag = 0
|
||||
oflag = 0
|
||||
lflag = 0
|
||||
return [iflag, oflag, cflag, lflag, ispeed, ospeed, cc]
|
||||
|
||||
|
||||
def disable_echo(fd):
|
||||
old = termios.tcgetattr(fd)
|
||||
new = cfmakeraw(old)
|
||||
flags = (
|
||||
termios.TCSAFLUSH |
|
||||
getattr(termios, 'TCSASOFT', 0)
|
||||
)
|
||||
termios.tcsetattr(fd, flags, new)
|
||||
|
||||
|
||||
def close_nonstandard_fds():
|
||||
for fd in xrange(3, 1024):
|
||||
try:
|
||||
os.close(fd)
|
||||
except OSError:
|
||||
pass
|
||||
|
||||
|
||||
def tty_create_child(*args):
|
||||
"""
|
||||
Return a file descriptor connected to the master end of a pseudo-terminal,
|
||||
whose slave end is connected to stdin/stdout/stderr of a new child process.
|
||||
The child is created such that the pseudo-terminal becomes its controlling
|
||||
TTY, ensuring access to /dev/tty returns a new file descriptor open on the
|
||||
slave end.
|
||||
|
||||
:param args:
|
||||
execl() arguments.
|
||||
"""
|
||||
master_fd, slave_fd = os.openpty()
|
||||
disable_echo(master_fd)
|
||||
disable_echo(slave_fd)
|
||||
|
||||
pid = os.fork()
|
||||
if not pid:
|
||||
os.dup2(slave_fd, 0)
|
||||
os.dup2(slave_fd, 1)
|
||||
os.dup2(slave_fd, 2)
|
||||
close_nonstandard_fds()
|
||||
os.setsid()
|
||||
os.close(os.open(os.ttyname(1), os.O_RDWR))
|
||||
os.execvp(args[0], args)
|
||||
raise SystemExit
|
||||
|
||||
os.close(slave_fd)
|
||||
LOG.debug('tty_create_child() child %d fd %d, parent %d, args %r',
|
||||
pid, master_fd, os.getpid(), args)
|
||||
return pid, master_fd
|
||||
|
||||
|
||||
def write_all(fd, s):
|
||||
written = 0
|
||||
while written < len(s):
|
||||
|
@ -105,8 +179,8 @@ def iter_read(fd, deadline):
|
|||
|
||||
if not s:
|
||||
raise mitogen.core.StreamError(
|
||||
'EOF on stream; last 100 bytes received: %r' %
|
||||
(''.join(bits)[-100:],)
|
||||
'EOF on stream; last 300 bytes received: %r' %
|
||||
(''.join(bits)[-300:],)
|
||||
)
|
||||
|
||||
bits.append(s)
|
||||
|
|
|
@ -3,36 +3,58 @@ Functionality to allow establishing new slave contexts over an SSH connection.
|
|||
"""
|
||||
|
||||
import commands
|
||||
import logging
|
||||
import time
|
||||
|
||||
import mitogen.master
|
||||
|
||||
|
||||
LOG = logging.getLogger('mitogen')
|
||||
|
||||
PASSWORD_PROMPT = 'password'
|
||||
PERMDENIED_PROMPT = 'permission denied'
|
||||
|
||||
|
||||
class PasswordError(mitogen.core.Error):
|
||||
pass
|
||||
|
||||
|
||||
class Stream(mitogen.master.Stream):
|
||||
python_path = 'python'
|
||||
create_child = staticmethod(mitogen.master.tty_create_child)
|
||||
python_path = 'python2.7'
|
||||
|
||||
#: The path to the SSH binary.
|
||||
ssh_path = 'ssh'
|
||||
|
||||
identity_file = None
|
||||
password = None
|
||||
port = None
|
||||
|
||||
def construct(self, hostname, username=None, ssh_path=None, port=None,
|
||||
check_host_keys=True, **kwargs):
|
||||
check_host_keys=True, password=None, identity_file=None,
|
||||
**kwargs):
|
||||
super(Stream, self).construct(**kwargs)
|
||||
self.hostname = hostname
|
||||
self.username = username
|
||||
self.port = port
|
||||
self.check_host_keys = check_host_keys
|
||||
self.password = password
|
||||
self.identity_file = identity_file
|
||||
if ssh_path:
|
||||
self.ssh_path = ssh_path
|
||||
|
||||
def get_boot_command(self):
|
||||
bits = [self.ssh_path]
|
||||
bits += ['-o', 'BatchMode yes']
|
||||
#bits += ['-o', 'BatchMode yes']
|
||||
|
||||
if self.username:
|
||||
bits += ['-l', self.username]
|
||||
if self.port is not None:
|
||||
bits += ['-p', str(self.port)]
|
||||
if self.identity_file or self.password:
|
||||
bits += ['-o', 'IdentitiesOnly yes']
|
||||
if self.identity_file:
|
||||
bits += ['-i', self.identity_file]
|
||||
if not self.check_host_keys:
|
||||
bits += [
|
||||
'-o', 'StrictHostKeyChecking no',
|
||||
|
@ -47,3 +69,27 @@ class Stream(mitogen.master.Stream):
|
|||
self.name = 'ssh.' + self.hostname
|
||||
if self.port:
|
||||
self.name += ':%s' % (self.port,)
|
||||
|
||||
password_incorrect_msg = 'SSH password is incorrect'
|
||||
password_required_msg = 'SSH password was requested, but none specified'
|
||||
|
||||
def _connect_bootstrap(self):
|
||||
password_sent = False
|
||||
for buf in mitogen.master.iter_read(self.receive_side.fd,
|
||||
time.time() + 10.0):
|
||||
LOG.debug('%r: received %r', self, buf)
|
||||
if buf.endswith('EC0\n'):
|
||||
return self._ec0_received()
|
||||
elif PERMDENIED_PROMPT in buf.lower():
|
||||
if self.password is not None and password_sent:
|
||||
raise PasswordError(self.password_incorrect_msg)
|
||||
else:
|
||||
raise PasswordError(self.auth_incorrect_msg)
|
||||
elif PASSWORD_PROMPT in buf.lower():
|
||||
if self.password is None:
|
||||
raise PasswordError(self.password_required_msg)
|
||||
LOG.debug('sending password')
|
||||
self.transmit_side.write(self.password + '\n')
|
||||
password_sent = True
|
||||
else:
|
||||
raise mitogen.core.StreamError('bootstrap failed')
|
||||
|
|
|
@ -1,8 +1,6 @@
|
|||
|
||||
import logging
|
||||
import os
|
||||
import pty
|
||||
import termios
|
||||
import time
|
||||
|
||||
import mitogen.core
|
||||
|
@ -17,80 +15,8 @@ class PasswordError(mitogen.core.Error):
|
|||
pass
|
||||
|
||||
|
||||
def flags(names):
|
||||
"""Return the result of ORing a set of (space separated) :py:mod:`termios`
|
||||
module constants together."""
|
||||
return sum(getattr(termios, name) for name in names.split())
|
||||
|
||||
|
||||
def cfmakeraw((iflag, oflag, cflag, lflag, ispeed, ospeed, cc)):
|
||||
"""Given a list returned by :py:func:`termios.tcgetattr`, return a list
|
||||
that has been modified in the same manner as the `cfmakeraw()` C library
|
||||
function."""
|
||||
iflag &= ~flags('IGNBRK BRKINT PARMRK ISTRIP INLCR IGNCR ICRNL IXON')
|
||||
oflag &= ~flags('OPOST IXOFF')
|
||||
lflag &= ~flags('ECHO ECHOE ECHONL ICANON ISIG IEXTEN')
|
||||
cflag &= ~flags('CSIZE PARENB')
|
||||
cflag |= flags('CS8')
|
||||
|
||||
iflag = 0
|
||||
oflag = 0
|
||||
lflag = 0
|
||||
return [iflag, oflag, cflag, lflag, ispeed, ospeed, cc]
|
||||
|
||||
|
||||
def disable_echo(fd):
|
||||
old = termios.tcgetattr(fd)
|
||||
new = cfmakeraw(old)
|
||||
flags = (
|
||||
termios.TCSAFLUSH |
|
||||
getattr(termios, 'TCSASOFT', 0)
|
||||
)
|
||||
termios.tcsetattr(fd, flags, new)
|
||||
|
||||
|
||||
def close_nonstandard_fds():
|
||||
for fd in xrange(3, 1024):
|
||||
try:
|
||||
os.close(fd)
|
||||
except OSError:
|
||||
pass
|
||||
|
||||
|
||||
def tty_create_child(*args):
|
||||
"""
|
||||
Return a file descriptor connected to the master end of a pseudo-terminal,
|
||||
whose slave end is connected to stdin/stdout/stderr of a new child process.
|
||||
The child is created such that the pseudo-terminal becomes its controlling
|
||||
TTY, ensuring access to /dev/tty returns a new file descriptor open on the
|
||||
slave end.
|
||||
|
||||
:param args:
|
||||
execl() arguments.
|
||||
"""
|
||||
master_fd, slave_fd = os.openpty()
|
||||
disable_echo(master_fd)
|
||||
disable_echo(slave_fd)
|
||||
|
||||
pid = os.fork()
|
||||
if not pid:
|
||||
os.dup2(slave_fd, 0)
|
||||
os.dup2(slave_fd, 1)
|
||||
os.dup2(slave_fd, 2)
|
||||
close_nonstandard_fds()
|
||||
os.setsid()
|
||||
os.close(os.open(os.ttyname(1), os.O_RDWR))
|
||||
os.execvp(args[0], args)
|
||||
raise SystemExit
|
||||
|
||||
os.close(slave_fd)
|
||||
LOG.debug('tty_create_child() child %d fd %d, parent %d, args %r',
|
||||
pid, master_fd, os.getpid(), args)
|
||||
return pid, master_fd
|
||||
|
||||
|
||||
class Stream(mitogen.master.Stream):
|
||||
create_child = staticmethod(tty_create_child)
|
||||
create_child = staticmethod(mitogen.master.tty_create_child)
|
||||
sudo_path = 'sudo'
|
||||
password = None
|
||||
|
||||
|
|
Loading…
Reference in New Issue