508d462ddac438729a032c404400c57d36eb2a97
[nepi.git] / src / nepi / util / sshfuncs.py
1 #
2 #    NEPI, a framework to manage network experiments
3 #    Copyright (C) 2013 INRIA
4 #
5 #    This program is free software: you can redistribute it and/or modify
6 #    it under the terms of the GNU General Public License as published by
7 #    the Free Software Foundation, either version 3 of the License, or
8 #    (at your option) any later version.
9 #
10 #    This program is distributed in the hope that it will be useful,
11 #    but WITHOUT ANY WARRANTY; without even the implied warranty of
12 #    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
13 #    GNU General Public License for more details.
14 #
15 #    You should have received a copy of the GNU General Public License
16 #    along with this program.  If not, see <http://www.gnu.org/licenses/>.
17 #
18 # Author: Alina Quereilhac <alina.quereilhac@inria.fr>
19 #         Claudio Freire <claudio-daniel.freire@inria.fr>
20
21 ## TODO: This code needs reviewing !!!
22
23 import base64
24 import errno
25 import hashlib
26 import logging
27 import os
28 import os.path
29 import re
30 import select
31 import signal
32 import socket
33 import subprocess
34 import threading
35 import time
36 import tempfile
37
38 _re_inet = re.compile("\d+:\s+(?P<name>[a-z0-9_-]+)\s+inet6?\s+(?P<inet>[a-f0-9.:/]+)\s+(brd\s+[0-9.]+)?.*scope\s+global.*") 
39
40 logger = logging.getLogger("sshfuncs")
41
42 def log(msg, level, out = None, err = None):
43     if out:
44         msg += " - OUT: %s " % out
45
46     if err:
47         msg += " - ERROR: %s " % err
48
49     logger.log(level, msg)
50
51 if hasattr(os, "devnull"):
52     DEV_NULL = os.devnull
53 else:
54     DEV_NULL = "/dev/null"
55
56 SHELL_SAFE = re.compile('^[-a-zA-Z0-9_=+:.,/]*$')
57
58 class STDOUT: 
59     """
60     Special value that when given to rspawn in stderr causes stderr to 
61     redirect to whatever stdout was redirected to.
62     """
63
64 class ProcStatus:
65     """
66     Codes for status of remote spawned process
67     """
68     # Process is still running
69     RUNNING = 1
70
71     # Process is finished
72     FINISHED = 2
73     
74     # Process hasn't started running yet (this should be very rare)
75     NOT_STARTED = 3
76
77 hostbyname_cache = dict()
78 hostbyname_cache_lock = threading.Lock()
79
80 def resolve_hostname(host):
81     ip = None
82
83     if host in ["localhost", "127.0.0.1", "::1"]:
84         try:
85            ip = socket.gethostbyname(socket.gethostname())
86         except socket.gaierror, e: #[Errno -5] No address associated with hostname
87             p = subprocess.Popen("ip -o addr list", shell=True,
88                     stdout=subprocess.PIPE, stderr=subprocess.PIPE)
89             stdout, stderr = p.communicate()
90             m = _re_inet.findall(stdout)
91             ip = m[0][1].split("/")[0]
92     else:
93         ip = socket.gethostbyname(host)
94
95     return ip
96
97 def gethostbyname(host):
98     global hostbyname_cache
99     global hostbyname_cache_lock
100     
101     hostbyname = hostbyname_cache.get(host)
102     if not hostbyname:
103         with hostbyname_cache_lock:
104             hostbyname = resolve_hostname(host)
105             hostbyname_cache[host] = hostbyname
106
107             msg = " Added hostbyname %s - %s " % (host, hostbyname)
108             log(msg, logging.DEBUG)
109
110     return hostbyname
111
112 OPENSSH_HAS_PERSIST = None
113
114 def openssh_has_persist():
115     """ The ssh_config options ControlMaster and ControlPersist allow to
116     reuse a same network connection for multiple ssh sessions. In this 
117     way limitations on number of open ssh connections can be bypassed.
118     However, older versions of openSSH do not support this feature.
119     This function is used to determine if ssh connection persist features
120     can be used.
121     """
122     global OPENSSH_HAS_PERSIST
123     if OPENSSH_HAS_PERSIST is None:
124         proc = subprocess.Popen(["ssh","-v"],
125             stdout = subprocess.PIPE,
126             stderr = subprocess.STDOUT,
127             stdin = open("/dev/null","r") )
128         out,err = proc.communicate()
129         proc.wait()
130         
131         vre = re.compile(r'OpenSSH_(?:[6-9]|5[.][8-9]|5[.][1-9][0-9]|[1-9][0-9]).*', re.I)
132         OPENSSH_HAS_PERSIST = bool(vre.match(out))
133     return OPENSSH_HAS_PERSIST
134
135 def make_server_key_args(server_key, host, port):
136     """ Returns a reference to a temporary known_hosts file, to which 
137     the server key has been added. 
138     
139     Make sure to hold onto the temp file reference until the process is 
140     done with it
141
142     :param server_key: the server public key
143     :type server_key: str
144
145     :param host: the hostname
146     :type host: str
147
148     :param port: the ssh port
149     :type port: str
150
151     """
152     if port is not None:
153         host = '%s:%s' % (host, str(port))
154
155     # Create a temporary server key file
156     tmp_known_hosts = tempfile.NamedTemporaryFile()
157    
158     hostbyname = gethostbyname(host) 
159
160     # Add the intended host key
161     tmp_known_hosts.write('%s,%s %s\n' % (host, hostbyname, server_key))
162     
163     # If we're not in strict mode, add user-configured keys
164     if os.environ.get('NEPI_STRICT_AUTH_MODE',"").lower() not in ('1','true','on'):
165         user_hosts_path = '%s/.ssh/known_hosts' % (os.environ.get('HOME',""),)
166         if os.access(user_hosts_path, os.R_OK):
167             f = open(user_hosts_path, "r")
168             tmp_known_hosts.write(f.read())
169             f.close()
170         
171     tmp_known_hosts.flush()
172     
173     return tmp_known_hosts
174
175 def make_control_path(agent, forward_x11):
176     ctrl_path = "/tmp/nepi_ssh"
177
178     if agent:
179         ctrl_path +="_a"
180
181     if forward_x11:
182         ctrl_path +="_x"
183
184     ctrl_path += "-%r@%h:%p"
185
186     return ctrl_path
187
188 def shell_escape(s):
189     """ Escapes strings so that they are safe to use as command-line 
190     arguments """
191     if SHELL_SAFE.match(s):
192         # safe string - no escaping needed
193         return s
194     else:
195         # unsafe string - escape
196         def escp(c):
197             if (32 <= ord(c) < 127 or c in ('\r','\n','\t')) and c not in ("'",'"'):
198                 return c
199             else:
200                 return "'$'\\x%02x''" % (ord(c),)
201         s = ''.join(map(escp,s))
202         return "'%s'" % (s,)
203
204 def eintr_retry(func):
205     """Retries a function invocation when a EINTR occurs"""
206     import functools
207     @functools.wraps(func)
208     def rv(*p, **kw):
209         retry = kw.pop("_retry", False)
210         for i in xrange(0 if retry else 4):
211             try:
212                 return func(*p, **kw)
213             except (select.error, socket.error), args:
214                 if args[0] == errno.EINTR:
215                     continue
216                 else:
217                     raise 
218             except OSError, e:
219                 if e.errno == errno.EINTR:
220                     continue
221                 else:
222                     raise
223         else:
224             return func(*p, **kw)
225     return rv
226
227 def rexec(command, host, user, 
228         port = None,
229         gwuser = None,
230         gw = None, 
231         agent = True,
232         sudo = False,
233         identity = None,
234         server_key = None,
235         env = None,
236         tty = False,
237         connect_timeout = 30,
238         retry = 3,
239         persistent = True,
240         forward_x11 = False,
241         blocking = True,
242         strict_host_checking = True):
243     """
244     Executes a remote command, returns ((stdout,stderr),process)
245     """
246
247     tmp_known_hosts = None
248     if not gw:
249         hostip = gethostbyname(host)
250     else: hostip = None
251
252     args = ['ssh', '-C',
253             # Don't bother with localhost. Makes test easier
254             '-o', 'NoHostAuthenticationForLocalhost=yes',
255             '-o', 'ConnectTimeout=%d' % (int(connect_timeout),),
256             '-o', 'ConnectionAttempts=3',
257             '-o', 'ServerAliveInterval=30',
258             '-o', 'TCPKeepAlive=yes',
259             '-o', 'Batchmode=yes',
260             '-l', user, hostip or host]
261
262     if persistent and openssh_has_persist():
263         args.extend([
264             '-o', 'ControlMaster=auto',
265             '-o', 'ControlPath=%s' % (make_control_path(agent, forward_x11),),
266             '-o', 'ControlPersist=60' ])
267
268     if not strict_host_checking:
269         # Do not check for Host key. Unsafe.
270         args.extend(['-o', 'StrictHostKeyChecking=no'])
271
272     if gw:
273         proxycommand = _proxy_command(gw, gwuser, identity)
274         args.extend(['-o', proxycommand])
275
276     if agent:
277         args.append('-A')
278
279     if port:
280         args.append('-p%d' % port)
281
282     if identity:
283         identity = os.path.expanduser(identity)
284         args.extend(('-i', identity))
285
286     if tty:
287         args.append('-t')
288         args.append('-t')
289
290     if forward_x11:
291         args.append('-X')
292
293     if server_key:
294         # Create a temporary server key file
295         tmp_known_hosts = make_server_key_args(server_key, host, port)
296         args.extend(['-o', 'UserKnownHostsFile=%s' % (tmp_known_hosts.name,)])
297
298     if sudo:
299         command = "sudo " + command
300
301     args.append(command)
302
303     log_msg = " rexec - host %s - command %s " % (str(host), " ".join(map(str, args))) 
304
305     stdout = stderr = stdin = subprocess.PIPE
306     if forward_x11:
307         stdout = stderr = stdin = None
308
309     return _retry_rexec(args, log_msg, 
310             stderr = stderr,
311             stdin = stdin,
312             stdout = stdout,
313             env = env, 
314             retry = retry, 
315             tmp_known_hosts = tmp_known_hosts,
316             blocking = blocking)
317
318 def rcopy(source, dest,
319         port = None,
320         gwuser = None,
321         gw = None,
322         recursive = False,
323         identity = None,
324         server_key = None,
325         retry = 3,
326         strict_host_checking = True):
327     """
328     Copies from/to remote sites.
329     
330     Source and destination should have the user and host encoded
331     as per scp specs.
332     
333     Source can be a list of files to copy to a single destination, 
334     (in which case it is advised that the destination be a folder),
335     or a single file in a string.
336     """
337
338     # Parse destination as <user>@<server>:<path>
339     if isinstance(dest, str) and ':' in dest:
340         remspec, path = dest.split(':',1)
341     elif isinstance(source, str) and ':' in source:
342         remspec, path = source.split(':',1)
343     else:
344         raise ValueError, "Both endpoints cannot be local"
345     user,host = remspec.rsplit('@',1)
346     
347     # plain scp
348     tmp_known_hosts = None
349
350     args = ['scp', '-q', '-p', '-C',
351             # Speed up transfer using blowfish cypher specification which is 
352             # faster than the default one (3des)
353             '-c', 'blowfish',
354             # Don't bother with localhost. Makes test easier
355             '-o', 'NoHostAuthenticationForLocalhost=yes',
356             '-o', 'ConnectTimeout=60',
357             '-o', 'ConnectionAttempts=3',
358             '-o', 'ServerAliveInterval=30',
359             '-o', 'TCPKeepAlive=yes' ]
360             
361     if port:
362         args.append('-P%d' % port)
363
364     if gw:
365         proxycommand = _proxy_command(gw, gwuser, identity)
366         args.extend(['-o', proxycommand])
367
368     if recursive:
369         args.append('-r')
370
371     if identity:
372         identity = os.path.expanduser(identity)
373         args.extend(('-i', identity))
374
375     if server_key:
376         # Create a temporary server key file
377         tmp_known_hosts = make_server_key_args(server_key, host, port)
378         args.extend(['-o', 'UserKnownHostsFile=%s' % (tmp_known_hosts.name,)])
379
380     if not strict_host_checking:
381         # Do not check for Host key. Unsafe.
382         args.extend(['-o', 'StrictHostKeyChecking=no'])
383     
384     if isinstance(source, list):
385         args.extend(source)
386     else:
387         if openssh_has_persist():
388             args.extend([
389                 '-o', 'ControlMaster=auto',
390                 '-o', 'ControlPath=%s' % (make_control_path(False, False),)
391                 ])
392         args.append(source)
393
394     if isinstance(dest, list):
395         args.extend(dest)
396     else:
397         args.append(dest)
398
399     log_msg = " rcopy - host %s - command %s " % (str(host), " ".join(map(str, args)))
400     
401     return _retry_rexec(args, log_msg, env = None, retry = retry, 
402             tmp_known_hosts = tmp_known_hosts,
403             blocking = True)
404
405 def rspawn(command, pidfile, 
406         stdout = '/dev/null', 
407         stderr = STDOUT, 
408         stdin = '/dev/null',
409         home = None, 
410         create_home = False, 
411         sudo = False,
412         host = None, 
413         port = None, 
414         user = None, 
415         gwuser = None,
416         gw = None,
417         agent = None, 
418         identity = None, 
419         server_key = None,
420         tty = False,
421         strict_host_checking = True):
422     """
423     Spawn a remote command such that it will continue working asynchronously in 
424     background. 
425
426         :param command: The command to run, it should be a single line.
427         :type command: str
428
429         :param pidfile: Path to a file where to store the pid and ppid of the 
430                         spawned process
431         :type pidfile: str
432
433         :param stdout: Path to file to redirect standard output. 
434                        The default value is /dev/null
435         :type stdout: str
436
437         :param stderr: Path to file to redirect standard error.
438                        If the special STDOUT value is used, stderr will 
439                        be redirected to the same file as stdout
440         :type stderr: str
441
442         :param stdin: Path to a file with input to be piped into the command's standard input
443         :type stdin: str
444
445         :param home: Path to working directory folder. 
446                     It is assumed to exist unless the create_home flag is set.
447         :type home: str
448
449         :param create_home: Flag to force creation of the home folder before 
450                             running the command
451         :type create_home: bool
452  
453         :param sudo: Flag forcing execution with sudo user
454         :type sudo: bool
455         
456         :rtype: tuple
457
458         (stdout, stderr), process
459         
460         Of the spawning process, which only captures errors at spawning time.
461         Usually only useful for diagnostics.
462     """
463     # Start process in a "daemonized" way, using nohup and heavy
464     # stdin/out redirection to avoid connection issues
465     if stderr is STDOUT:
466         stderr = '&1'
467     else:
468         stderr = ' ' + stderr
469     
470     daemon_command = '{ { %(command)s > %(stdout)s 2>%(stderr)s < %(stdin)s & } ; echo $! 1 > %(pidfile)s ; }' % {
471         'command' : command,
472         'pidfile' : shell_escape(pidfile),
473         'stdout' : stdout,
474         'stderr' : stderr,
475         'stdin' : stdin,
476     }
477     
478     cmd = "%(create)s%(gohome)s rm -f %(pidfile)s ; %(sudo)s nohup bash -c %(command)s " % {
479             'command' : shell_escape(daemon_command),
480             'sudo' : 'sudo -S' if sudo else '',
481             'pidfile' : shell_escape(pidfile),
482             'gohome' : 'cd %s ; ' % (shell_escape(home),) if home else '',
483             'create' : 'mkdir -p %s ; ' % (shell_escape(home),) if create_home and home else '',
484         }
485
486     (out,err),proc = rexec(
487         cmd,
488         host = host,
489         port = port,
490         user = user,
491         gwuser = gwuser,
492         gw = gw,
493         agent = agent,
494         identity = identity,
495         server_key = server_key,
496         tty = tty,
497         strict_host_checking = strict_host_checking ,
498         )
499     
500     if proc.wait():
501         raise RuntimeError, "Failed to set up application on host %s: %s %s" % (host, out,err,)
502
503     return ((out, err), proc)
504
505 @eintr_retry
506 def rgetpid(pidfile,
507         host = None, 
508         port = None, 
509         user = None, 
510         gwuser = None,
511         gw = None,
512         agent = None, 
513         identity = None,
514         server_key = None,
515         strict_host_checking = True):
516     """
517     Returns the pid and ppid of a process from a remote file where the 
518     information was stored.
519
520         :param home: Path to directory where the pidfile is located
521         :type home: str
522
523         :param pidfile: Name of file containing the pid information
524         :type pidfile: str
525         
526         :rtype: int
527         
528         A (pid, ppid) tuple useful for calling rstatus and rkill,
529         or None if the pidfile isn't valid yet (can happen when process is staring up)
530
531     """
532     (out,err),proc = rexec(
533         "cat %(pidfile)s" % {
534             'pidfile' : pidfile,
535         },
536         host = host,
537         port = port,
538         user = user,
539         gwuser = gwuser,
540         gw = gw,
541         agent = agent,
542         identity = identity,
543         server_key = server_key,
544         strict_host_checking = strict_host_checking
545         )
546         
547     if proc.wait():
548         return None
549     
550     if out:
551         try:
552             return map(int,out.strip().split(' ',1))
553         except:
554             # Ignore, many ways to fail that don't matter that much
555             return None
556
557 @eintr_retry
558 def rstatus(pid, ppid, 
559         host = None, 
560         port = None, 
561         user = None, 
562         gwuser = None,
563         gw = None,
564         agent = None, 
565         identity = None,
566         server_key = None,
567         strict_host_checking = True):
568     """
569     Returns a code representing the the status of a remote process
570
571         :param pid: Process id of the process
572         :type pid: int
573
574         :param ppid: Parent process id of process
575         :type ppid: int
576     
577         :rtype: int (One of NOT_STARTED, RUNNING, FINISHED)
578     
579     """
580     (out,err),proc = rexec(
581         # Check only by pid. pid+ppid does not always work (especially with sudo) 
582         " (( ps --pid %(pid)d -o pid | grep -c %(pid)d && echo 'wait')  || echo 'done' ) | tail -n 1" % {
583             'ppid' : ppid,
584             'pid' : pid,
585         },
586         host = host,
587         port = port,
588         user = user,
589         gwuser = gwuser,
590         gw = gw,
591         agent = agent,
592         identity = identity,
593         server_key = server_key,
594         strict_host_checking = strict_host_checking
595         )
596     
597     if proc.wait():
598         return ProcStatus.NOT_STARTED
599     
600     status = False
601     if err:
602         if err.strip().find("Error, do this: mount -t proc none /proc") >= 0:
603             status = True
604     elif out:
605         status = (out.strip() == 'wait')
606     else:
607         return ProcStatus.NOT_STARTED
608     return ProcStatus.RUNNING if status else ProcStatus.FINISHED
609
610 @eintr_retry
611 def rkill(pid, ppid,
612         host = None, 
613         port = None, 
614         user = None, 
615         gwuser = None,
616         gw = None,
617         agent = None, 
618         sudo = False,
619         identity = None, 
620         server_key = None, 
621         nowait = False,
622         strict_host_checking = True):
623     """
624     Sends a kill signal to a remote process.
625
626     First tries a SIGTERM, and if the process does not end in 10 seconds,
627     it sends a SIGKILL.
628  
629         :param pid: Process id of process to be killed
630         :type pid: int
631
632         :param ppid: Parent process id of process to be killed
633         :type ppid: int
634
635         :param sudo: Flag indicating if sudo should be used to kill the process
636         :type sudo: bool
637         
638     """
639     subkill = "$(ps --ppid %(pid)d -o pid h)" % { 'pid' : pid }
640     cmd = """
641 SUBKILL="%(subkill)s" ;
642 %(sudo)s kill -- -%(pid)d $SUBKILL || /bin/true
643 %(sudo)s kill %(pid)d $SUBKILL || /bin/true
644 for x in 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 ; do 
645     sleep 0.2 
646     if [ `ps --pid %(pid)d -o pid | grep -c %(pid)d` == '0' ]; then
647         break
648     else
649         %(sudo)s kill -- -%(pid)d $SUBKILL || /bin/true
650         %(sudo)s kill %(pid)d $SUBKILL || /bin/true
651     fi
652     sleep 1.8
653 done
654 if [ `ps --pid %(pid)d -o pid | grep -c %(pid)d` != '0' ]; then
655     %(sudo)s kill -9 -- -%(pid)d $SUBKILL || /bin/true
656     %(sudo)s kill -9 %(pid)d $SUBKILL || /bin/true
657 fi
658 """
659     if nowait:
660         cmd = "( %s ) >/dev/null 2>/dev/null </dev/null &" % (cmd,)
661
662     (out,err),proc = rexec(
663         cmd % {
664             'ppid' : ppid,
665             'pid' : pid,
666             'sudo' : 'sudo -S' if sudo else '',
667             'subkill' : subkill,
668         },
669         host = host,
670         port = port,
671         user = user,
672         gwuser = gwuser,
673         gw = gw,
674         agent = agent,
675         identity = identity,
676         server_key = server_key,
677         strict_host_checking = strict_host_checking
678         )
679     
680     # wait, don't leave zombies around
681     proc.wait()
682
683     return (out, err), proc
684
685 def _retry_rexec(args,
686         log_msg,
687         stdout = subprocess.PIPE,
688         stdin = subprocess.PIPE, 
689         stderr = subprocess.PIPE,
690         env = None,
691         retry = 3,
692         tmp_known_hosts = None,
693         blocking = True):
694
695     for x in xrange(retry):
696         # connects to the remote host and starts a remote connection
697         proc = subprocess.Popen(args,
698                 env = env,
699                 stdout = stdout,
700                 stdin = stdin, 
701                 stderr = stderr)
702         
703         # attach tempfile object to the process, to make sure the file stays
704         # alive until the process is finished with it
705         proc._known_hosts = tmp_known_hosts
706     
707         # The argument block == False forces to rexec to return immediately, 
708         # without blocking 
709         try:
710             err = out = " "
711             if blocking:
712                 #(out, err) = proc.communicate()
713                 # The method communicate was re implemented for performance issues
714                 # when using python subprocess communicate method the ssh commands 
715                 # last one minute each
716                 out, err = _communicate(proc, input=None)
717
718             elif stdout:
719                 out = proc.stdout.read()
720                 if proc.poll() and stderr:
721                     err = proc.stderr.read()
722
723             log(log_msg, logging.DEBUG, out, err)
724
725             if proc.poll():
726                 skip = False
727
728                 if err.strip().startswith('ssh: ') or err.strip().startswith('mux_client_hello_exchange: '):
729                     # SSH error, can safely retry
730                     skip = True 
731                 elif retry:
732                     # Probably timed out or plain failed but can retry
733                     skip = True 
734                 
735                 if skip:
736                     t = x*2
737                     msg = "SLEEPING %d ... ATEMPT %d - command %s " % ( 
738                             t, x, " ".join(args))
739                     log(msg, logging.DEBUG)
740
741                     time.sleep(t)
742                     continue
743             break
744         except RuntimeError, e:
745             msg = " rexec EXCEPTION - TIMEOUT -> %s \n %s" % ( e.args, log_msg )
746             log(msg, logging.DEBUG, out, err)
747
748             if retry <= 0:
749                 raise
750             retry -= 1
751
752     return ((out, err), proc)
753
754 # POSIX
755 # Don't remove. The method communicate was re implemented for performance issues
756 def _communicate(proc, input, timeout=None, err_on_timeout=True):
757     read_set = []
758     write_set = []
759     stdout = None # Return
760     stderr = None # Return
761
762     killed = False
763
764     if timeout is not None:
765         timelimit = time.time() + timeout
766         killtime = timelimit + 4
767         bailtime = timelimit + 4
768
769     if proc.stdin:
770         # Flush stdio buffer.  This might block, if the user has
771         # been writing to .stdin in an uncontrolled fashion.
772         proc.stdin.flush()
773         if input:
774             write_set.append(proc.stdin)
775         else:
776             proc.stdin.close()
777
778     if proc.stdout:
779         read_set.append(proc.stdout)
780         stdout = []
781
782     if proc.stderr:
783         read_set.append(proc.stderr)
784         stderr = []
785
786     input_offset = 0
787     while read_set or write_set:
788         if timeout is not None:
789             curtime = time.time()
790             if timeout is None or curtime > timelimit:
791                 if curtime > bailtime:
792                     break
793                 elif curtime > killtime:
794                     signum = signal.SIGKILL
795                 else:
796                     signum = signal.SIGTERM
797                 # Lets kill it
798                 os.kill(proc.pid, signum)
799                 select_timeout = 0.5
800             else:
801                 select_timeout = timelimit - curtime + 0.1
802         else:
803             select_timeout = 1.0
804
805         if select_timeout > 1.0:
806             select_timeout = 1.0
807
808         try:
809             rlist, wlist, xlist = select.select(read_set, write_set, [], select_timeout)
810         except select.error,e:
811             if e[0] != 4:
812                 raise
813             else:
814                 continue
815
816         if not rlist and not wlist and not xlist and proc.poll() is not None:
817             # timeout and process exited, say bye
818             break
819
820         if proc.stdin in wlist:
821             # When select has indicated that the file is writable,
822             # we can write up to PIPE_BUF bytes without risk
823             # blocking.  POSIX defines PIPE_BUF >= 512
824             bytes_written = os.write(proc.stdin.fileno(),
825                     buffer(input, input_offset, 512))
826             input_offset += bytes_written
827
828             if input_offset >= len(input):
829                 proc.stdin.close()
830                 write_set.remove(proc.stdin)
831
832         if proc.stdout in rlist:
833             data = os.read(proc.stdout.fileno(), 1024)
834             if data == "":
835                 proc.stdout.close()
836                 read_set.remove(proc.stdout)
837             stdout.append(data)
838
839         if proc.stderr in rlist:
840             data = os.read(proc.stderr.fileno(), 1024)
841             if data == "":
842                 proc.stderr.close()
843                 read_set.remove(proc.stderr)
844             stderr.append(data)
845
846     # All data exchanged.  Translate lists into strings.
847     if stdout is not None:
848         stdout = ''.join(stdout)
849     if stderr is not None:
850         stderr = ''.join(stderr)
851
852     # Translate newlines, if requested.  We cannot let the file
853     # object do the translation: It is based on stdio, which is
854     # impossible to combine with select (unless forcing no
855     # buffering).
856     if proc.universal_newlines and hasattr(file, 'newlines'):
857         if stdout:
858             stdout = proc._translate_newlines(stdout)
859         if stderr:
860             stderr = proc._translate_newlines(stderr)
861
862     if killed and err_on_timeout:
863         errcode = proc.poll()
864         raise RuntimeError, ("Operation timed out", errcode, stdout, stderr)
865     else:
866         if killed:
867             proc.poll()
868         else:
869             proc.wait()
870         return (stdout, stderr)
871
872 def _proxy_command(gw, gwuser, gwidentity):
873     """
874     Constructs the SSH ProxyCommand option to add to the SSH command to connect
875     via a proxy
876         :param gw: SSH proxy hostname
877         :type gw:  str 
878        
879         :param gwuser: SSH proxy username
880         :type gwuser:  str
881
882         :param gwidentity: SSH proxy identity file 
883         :type gwidentity:  str
884
885   
886         :rtype: str 
887         
888         returns the SSH ProxyCommand option.
889     """
890
891     proxycommand = 'ProxyCommand=ssh -q '
892     if gwidentity:
893         proxycommand += '-i %s ' % os.path.expanduser(gwidentity)
894     if gwuser:
895         proxycommand += '%s' % gwuser
896     else:
897         proxycommand += '%r'
898     proxycommand += '@%s -W %%h:%%p' % gw
899
900     return proxycommand
901