~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

Viewing changes to bzrlib/transport/ssh.py

  • Committer: Alexander Belchenko
  • Date: 2008-03-11 08:49:42 UTC
  • mto: This revision was merged to the branch mainline in revision 3268.
  • Revision ID: bialix@ukr.net-20080311084942-w1w0w3v0m20p2pbc
use sys.version_info

Show diffs side-by-side

added added

removed removed

Lines of Context:
 
1
# Copyright (C) 2005 Robey Pointer <robey@lag.net>
 
2
# Copyright (C) 2005, 2006, 2007 Canonical Ltd
 
3
#
 
4
# This program is free software; you can redistribute it and/or modify
 
5
# it under the terms of the GNU General Public License as published by
 
6
# the Free Software Foundation; either version 2 of the License, or
 
7
# (at your option) any later version.
 
8
#
 
9
# This program is distributed in the hope that it will be useful,
 
10
# but WITHOUT ANY WARRANTY; without even the implied warranty of
 
11
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 
12
# GNU General Public License for more details.
 
13
#
 
14
# You should have received a copy of the GNU General Public License
 
15
# along with this program; if not, write to the Free Software
 
16
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
 
17
 
 
18
"""Foundation SSH support for SFTP and smart server."""
 
19
 
 
20
import errno
 
21
import getpass
 
22
import os
 
23
import socket
 
24
import subprocess
 
25
import sys
 
26
 
 
27
from bzrlib import (
 
28
    config,
 
29
    errors,
 
30
    osutils,
 
31
    trace,
 
32
    ui,
 
33
    )
 
34
 
 
35
try:
 
36
    import paramiko
 
37
except ImportError, e:
 
38
    # If we have an ssh subprocess, we don't strictly need paramiko for all ssh
 
39
    # access
 
40
    paramiko = None
 
41
else:
 
42
    from paramiko.sftp_client import SFTPClient
 
43
 
 
44
 
 
45
SYSTEM_HOSTKEYS = {}
 
46
BZR_HOSTKEYS = {}
 
47
 
 
48
 
 
49
_paramiko_version = getattr(paramiko, '__version_info__', (0, 0, 0))
 
50
 
 
51
# Paramiko 1.5 tries to open a socket.AF_UNIX in order to connect
 
52
# to ssh-agent. That attribute doesn't exist on win32 (it does in cygwin)
 
53
# so we get an AttributeError exception. So we will not try to
 
54
# connect to an agent if we are on win32 and using Paramiko older than 1.6
 
55
_use_ssh_agent = (sys.platform != 'win32' or _paramiko_version >= (1, 6, 0))
 
56
 
 
57
 
 
58
class SSHVendorManager(object):
 
59
    """Manager for manage SSH vendors."""
 
60
 
 
61
    # Note, although at first sign the class interface seems similar to
 
62
    # bzrlib.registry.Registry it is not possible/convenient to directly use
 
63
    # the Registry because the class just has "get()" interface instead of the
 
64
    # Registry's "get(key)".
 
65
 
 
66
    def __init__(self):
 
67
        self._ssh_vendors = {}
 
68
        self._cached_ssh_vendor = None
 
69
        self._default_ssh_vendor = None
 
70
 
 
71
    def register_default_vendor(self, vendor):
 
72
        """Register default SSH vendor."""
 
73
        self._default_ssh_vendor = vendor
 
74
 
 
75
    def register_vendor(self, name, vendor):
 
76
        """Register new SSH vendor by name."""
 
77
        self._ssh_vendors[name] = vendor
 
78
 
 
79
    def clear_cache(self):
 
80
        """Clear previously cached lookup result."""
 
81
        self._cached_ssh_vendor = None
 
82
 
 
83
    def _get_vendor_by_environment(self, environment=None):
 
84
        """Return the vendor or None based on BZR_SSH environment variable.
 
85
 
 
86
        :raises UnknownSSH: if the BZR_SSH environment variable contains
 
87
                            unknown vendor name
 
88
        """
 
89
        if environment is None:
 
90
            environment = os.environ
 
91
        if 'BZR_SSH' in environment:
 
92
            vendor_name = environment['BZR_SSH']
 
93
            try:
 
94
                vendor = self._ssh_vendors[vendor_name]
 
95
            except KeyError:
 
96
                raise errors.UnknownSSH(vendor_name)
 
97
            return vendor
 
98
        return None
 
99
 
 
100
    def _get_ssh_version_string(self, args):
 
101
        """Return SSH version string from the subprocess."""
 
102
        try:
 
103
            p = subprocess.Popen(args,
 
104
                                 stdout=subprocess.PIPE,
 
105
                                 stderr=subprocess.PIPE,
 
106
                                 **os_specific_subprocess_params())
 
107
            stdout, stderr = p.communicate()
 
108
        except OSError:
 
109
            stdout = stderr = ''
 
110
        return stdout + stderr
 
111
 
 
112
    def _get_vendor_by_version_string(self, version, args):
 
113
        """Return the vendor or None based on output from the subprocess.
 
114
 
 
115
        :param version: The output of 'ssh -V' like command.
 
116
        :param args: Command line that was run.
 
117
        """
 
118
        vendor = None
 
119
        if 'OpenSSH' in version:
 
120
            trace.mutter('ssh implementation is OpenSSH')
 
121
            vendor = OpenSSHSubprocessVendor()
 
122
        elif 'SSH Secure Shell' in version:
 
123
            trace.mutter('ssh implementation is SSH Corp.')
 
124
            vendor = SSHCorpSubprocessVendor()
 
125
        elif 'plink' in version and args[0] == 'plink':
 
126
            # Checking if "plink" was the executed argument as Windows
 
127
            # sometimes reports 'ssh -V' incorrectly with 'plink' in it's
 
128
            # version.  See https://bugs.launchpad.net/bzr/+bug/107155
 
129
            trace.mutter("ssh implementation is Putty's plink.")
 
130
            vendor = PLinkSubprocessVendor()
 
131
        return vendor
 
132
 
 
133
    def _get_vendor_by_inspection(self):
 
134
        """Return the vendor or None by checking for known SSH implementations."""
 
135
        for args in (['ssh', '-V'], ['plink', '-V']):
 
136
            version = self._get_ssh_version_string(args)
 
137
            vendor = self._get_vendor_by_version_string(version, args)
 
138
            if vendor is not None:
 
139
                return vendor
 
140
        return None
 
141
 
 
142
    def get_vendor(self, environment=None):
 
143
        """Find out what version of SSH is on the system.
 
144
 
 
145
        :raises SSHVendorNotFound: if no any SSH vendor is found
 
146
        :raises UnknownSSH: if the BZR_SSH environment variable contains
 
147
                            unknown vendor name
 
148
        """
 
149
        if self._cached_ssh_vendor is None:
 
150
            vendor = self._get_vendor_by_environment(environment)
 
151
            if vendor is None:
 
152
                vendor = self._get_vendor_by_inspection()
 
153
                if vendor is None:
 
154
                    trace.mutter('falling back to default implementation')
 
155
                    vendor = self._default_ssh_vendor
 
156
                    if vendor is None:
 
157
                        raise errors.SSHVendorNotFound()
 
158
            self._cached_ssh_vendor = vendor
 
159
        return self._cached_ssh_vendor
 
160
 
 
161
_ssh_vendor_manager = SSHVendorManager()
 
162
_get_ssh_vendor = _ssh_vendor_manager.get_vendor
 
163
register_default_ssh_vendor = _ssh_vendor_manager.register_default_vendor
 
164
register_ssh_vendor = _ssh_vendor_manager.register_vendor
 
165
 
 
166
 
 
167
def _ignore_sigint():
 
168
    # TODO: This should possibly ignore SIGHUP as well, but bzr currently
 
169
    # doesn't handle it itself.
 
170
    # <https://launchpad.net/products/bzr/+bug/41433/+index>
 
171
    import signal
 
172
    signal.signal(signal.SIGINT, signal.SIG_IGN)
 
173
 
 
174
 
 
175
class LoopbackSFTP(object):
 
176
    """Simple wrapper for a socket that pretends to be a paramiko Channel."""
 
177
 
 
178
    def __init__(self, sock):
 
179
        self.__socket = sock
 
180
 
 
181
    def send(self, data):
 
182
        return self.__socket.send(data)
 
183
 
 
184
    def recv(self, n):
 
185
        return self.__socket.recv(n)
 
186
 
 
187
    def recv_ready(self):
 
188
        return True
 
189
 
 
190
    def close(self):
 
191
        self.__socket.close()
 
192
 
 
193
 
 
194
class SSHVendor(object):
 
195
    """Abstract base class for SSH vendor implementations."""
 
196
 
 
197
    def connect_sftp(self, username, password, host, port):
 
198
        """Make an SSH connection, and return an SFTPClient.
 
199
        
 
200
        :param username: an ascii string
 
201
        :param password: an ascii string
 
202
        :param host: a host name as an ascii string
 
203
        :param port: a port number
 
204
        :type port: int
 
205
 
 
206
        :raises: ConnectionError if it cannot connect.
 
207
 
 
208
        :rtype: paramiko.sftp_client.SFTPClient
 
209
        """
 
210
        raise NotImplementedError(self.connect_sftp)
 
211
 
 
212
    def connect_ssh(self, username, password, host, port, command):
 
213
        """Make an SSH connection.
 
214
        
 
215
        :returns: something with a `close` method, and a `get_filelike_channels`
 
216
            method that returns a pair of (read, write) filelike objects.
 
217
        """
 
218
        raise NotImplementedError(self.connect_ssh)
 
219
 
 
220
    def _raise_connection_error(self, host, port=None, orig_error=None,
 
221
                                msg='Unable to connect to SSH host'):
 
222
        """Raise a SocketConnectionError with properly formatted host.
 
223
 
 
224
        This just unifies all the locations that try to raise ConnectionError,
 
225
        so that they format things properly.
 
226
        """
 
227
        raise errors.SocketConnectionError(host=host, port=port, msg=msg,
 
228
                                           orig_error=orig_error)
 
229
 
 
230
 
 
231
class LoopbackVendor(SSHVendor):
 
232
    """SSH "vendor" that connects over a plain TCP socket, not SSH."""
 
233
 
 
234
    def connect_sftp(self, username, password, host, port):
 
235
        sock = socket.socket()
 
236
        try:
 
237
            sock.connect((host, port))
 
238
        except socket.error, e:
 
239
            self._raise_connection_error(host, port=port, orig_error=e)
 
240
        return SFTPClient(LoopbackSFTP(sock))
 
241
 
 
242
register_ssh_vendor('loopback', LoopbackVendor())
 
243
 
 
244
 
 
245
class _ParamikoSSHConnection(object):
 
246
    def __init__(self, channel):
 
247
        self.channel = channel
 
248
 
 
249
    def get_filelike_channels(self):
 
250
        return self.channel.makefile('rb'), self.channel.makefile('wb')
 
251
 
 
252
    def close(self):
 
253
        return self.channel.close()
 
254
 
 
255
 
 
256
class ParamikoVendor(SSHVendor):
 
257
    """Vendor that uses paramiko."""
 
258
 
 
259
    def _connect(self, username, password, host, port):
 
260
        global SYSTEM_HOSTKEYS, BZR_HOSTKEYS
 
261
 
 
262
        load_host_keys()
 
263
 
 
264
        try:
 
265
            t = paramiko.Transport((host, port or 22))
 
266
            t.set_log_channel('bzr.paramiko')
 
267
            t.start_client()
 
268
        except (paramiko.SSHException, socket.error), e:
 
269
            self._raise_connection_error(host, port=port, orig_error=e)
 
270
 
 
271
        server_key = t.get_remote_server_key()
 
272
        server_key_hex = paramiko.util.hexify(server_key.get_fingerprint())
 
273
        keytype = server_key.get_name()
 
274
        if host in SYSTEM_HOSTKEYS and keytype in SYSTEM_HOSTKEYS[host]:
 
275
            our_server_key = SYSTEM_HOSTKEYS[host][keytype]
 
276
            our_server_key_hex = paramiko.util.hexify(
 
277
                our_server_key.get_fingerprint())
 
278
        elif host in BZR_HOSTKEYS and keytype in BZR_HOSTKEYS[host]:
 
279
            our_server_key = BZR_HOSTKEYS[host][keytype]
 
280
            our_server_key_hex = paramiko.util.hexify(
 
281
                our_server_key.get_fingerprint())
 
282
        else:
 
283
            trace.warning('Adding %s host key for %s: %s'
 
284
                          % (keytype, host, server_key_hex))
 
285
            add = getattr(BZR_HOSTKEYS, 'add', None)
 
286
            if add is not None: # paramiko >= 1.X.X
 
287
                BZR_HOSTKEYS.add(host, keytype, server_key)
 
288
            else:
 
289
                BZR_HOSTKEYS.setdefault(host, {})[keytype] = server_key
 
290
            our_server_key = server_key
 
291
            our_server_key_hex = paramiko.util.hexify(
 
292
                our_server_key.get_fingerprint())
 
293
            save_host_keys()
 
294
        if server_key != our_server_key:
 
295
            filename1 = os.path.expanduser('~/.ssh/known_hosts')
 
296
            filename2 = osutils.pathjoin(config.config_dir(), 'ssh_host_keys')
 
297
            raise errors.TransportError(
 
298
                'Host keys for %s do not match!  %s != %s' %
 
299
                (host, our_server_key_hex, server_key_hex),
 
300
                ['Try editing %s or %s' % (filename1, filename2)])
 
301
 
 
302
        _paramiko_auth(username, password, host, port, t)
 
303
        return t
 
304
 
 
305
    def connect_sftp(self, username, password, host, port):
 
306
        t = self._connect(username, password, host, port)
 
307
        try:
 
308
            return t.open_sftp_client()
 
309
        except paramiko.SSHException, e:
 
310
            self._raise_connection_error(host, port=port, orig_error=e,
 
311
                                         msg='Unable to start sftp client')
 
312
 
 
313
    def connect_ssh(self, username, password, host, port, command):
 
314
        t = self._connect(username, password, host, port)
 
315
        try:
 
316
            channel = t.open_session()
 
317
            cmdline = ' '.join(command)
 
318
            channel.exec_command(cmdline)
 
319
            return _ParamikoSSHConnection(channel)
 
320
        except paramiko.SSHException, e:
 
321
            self._raise_connection_error(host, port=port, orig_error=e,
 
322
                                         msg='Unable to invoke remote bzr')
 
323
 
 
324
if paramiko is not None:
 
325
    vendor = ParamikoVendor()
 
326
    register_ssh_vendor('paramiko', vendor)
 
327
    register_ssh_vendor('none', vendor)
 
328
    register_default_ssh_vendor(vendor)
 
329
    _sftp_connection_errors = (EOFError, paramiko.SSHException)
 
330
    del vendor
 
331
else:
 
332
    _sftp_connection_errors = (EOFError,)
 
333
 
 
334
 
 
335
class SubprocessVendor(SSHVendor):
 
336
    """Abstract base class for vendors that use pipes to a subprocess."""
 
337
 
 
338
    def _connect(self, argv):
 
339
        proc = subprocess.Popen(argv,
 
340
                                stdin=subprocess.PIPE,
 
341
                                stdout=subprocess.PIPE,
 
342
                                **os_specific_subprocess_params())
 
343
        return SSHSubprocess(proc)
 
344
 
 
345
    def connect_sftp(self, username, password, host, port):
 
346
        try:
 
347
            argv = self._get_vendor_specific_argv(username, host, port,
 
348
                                                  subsystem='sftp')
 
349
            sock = self._connect(argv)
 
350
            return SFTPClient(sock)
 
351
        except _sftp_connection_errors, e:
 
352
            self._raise_connection_error(host, port=port, orig_error=e)
 
353
        except (OSError, IOError), e:
 
354
            # If the machine is fast enough, ssh can actually exit
 
355
            # before we try and send it the sftp request, which
 
356
            # raises a Broken Pipe
 
357
            if e.errno not in (errno.EPIPE,):
 
358
                raise
 
359
            self._raise_connection_error(host, port=port, orig_error=e)
 
360
 
 
361
    def connect_ssh(self, username, password, host, port, command):
 
362
        try:
 
363
            argv = self._get_vendor_specific_argv(username, host, port,
 
364
                                                  command=command)
 
365
            return self._connect(argv)
 
366
        except (EOFError), e:
 
367
            self._raise_connection_error(host, port=port, orig_error=e)
 
368
        except (OSError, IOError), e:
 
369
            # If the machine is fast enough, ssh can actually exit
 
370
            # before we try and send it the sftp request, which
 
371
            # raises a Broken Pipe
 
372
            if e.errno not in (errno.EPIPE,):
 
373
                raise
 
374
            self._raise_connection_error(host, port=port, orig_error=e)
 
375
 
 
376
    def _get_vendor_specific_argv(self, username, host, port, subsystem=None,
 
377
                                  command=None):
 
378
        """Returns the argument list to run the subprocess with.
 
379
        
 
380
        Exactly one of 'subsystem' and 'command' must be specified.
 
381
        """
 
382
        raise NotImplementedError(self._get_vendor_specific_argv)
 
383
 
 
384
 
 
385
class OpenSSHSubprocessVendor(SubprocessVendor):
 
386
    """SSH vendor that uses the 'ssh' executable from OpenSSH."""
 
387
 
 
388
    def _get_vendor_specific_argv(self, username, host, port, subsystem=None,
 
389
                                  command=None):
 
390
        assert subsystem is not None or command is not None, (
 
391
            'Must specify a command or subsystem')
 
392
        if subsystem is not None:
 
393
            assert command is None, (
 
394
                'subsystem and command are mutually exclusive')
 
395
        args = ['ssh',
 
396
                '-oForwardX11=no', '-oForwardAgent=no',
 
397
                '-oClearAllForwardings=yes', '-oProtocol=2',
 
398
                '-oNoHostAuthenticationForLocalhost=yes']
 
399
        if port is not None:
 
400
            args.extend(['-p', str(port)])
 
401
        if username is not None:
 
402
            args.extend(['-l', username])
 
403
        if subsystem is not None:
 
404
            args.extend(['-s', host, subsystem])
 
405
        else:
 
406
            args.extend([host] + command)
 
407
        return args
 
408
 
 
409
register_ssh_vendor('openssh', OpenSSHSubprocessVendor())
 
410
 
 
411
 
 
412
class SSHCorpSubprocessVendor(SubprocessVendor):
 
413
    """SSH vendor that uses the 'ssh' executable from SSH Corporation."""
 
414
 
 
415
    def _get_vendor_specific_argv(self, username, host, port, subsystem=None,
 
416
                                  command=None):
 
417
        assert subsystem is not None or command is not None, (
 
418
            'Must specify a command or subsystem')
 
419
        if subsystem is not None:
 
420
            assert command is None, (
 
421
                'subsystem and command are mutually exclusive')
 
422
        args = ['ssh', '-x']
 
423
        if port is not None:
 
424
            args.extend(['-p', str(port)])
 
425
        if username is not None:
 
426
            args.extend(['-l', username])
 
427
        if subsystem is not None:
 
428
            args.extend(['-s', subsystem, host])
 
429
        else:
 
430
            args.extend([host] + command)
 
431
        return args
 
432
 
 
433
register_ssh_vendor('ssh', SSHCorpSubprocessVendor())
 
434
 
 
435
 
 
436
class PLinkSubprocessVendor(SubprocessVendor):
 
437
    """SSH vendor that uses the 'plink' executable from Putty."""
 
438
 
 
439
    def _get_vendor_specific_argv(self, username, host, port, subsystem=None,
 
440
                                  command=None):
 
441
        assert subsystem is not None or command is not None, (
 
442
            'Must specify a command or subsystem')
 
443
        if subsystem is not None:
 
444
            assert command is None, (
 
445
                'subsystem and command are mutually exclusive')
 
446
        args = ['plink', '-x', '-a', '-ssh', '-2', '-batch']
 
447
        if port is not None:
 
448
            args.extend(['-P', str(port)])
 
449
        if username is not None:
 
450
            args.extend(['-l', username])
 
451
        if subsystem is not None:
 
452
            args.extend(['-s', host, subsystem])
 
453
        else:
 
454
            args.extend([host] + command)
 
455
        return args
 
456
 
 
457
register_ssh_vendor('plink', PLinkSubprocessVendor())
 
458
 
 
459
 
 
460
def _paramiko_auth(username, password, host, port, paramiko_transport):
 
461
    # paramiko requires a username, but it might be none if nothing was supplied
 
462
    # use the local username, just in case.
 
463
    # We don't override username, because if we aren't using paramiko,
 
464
    # the username might be specified in ~/.ssh/config and we don't want to
 
465
    # force it to something else
 
466
    # Also, it would mess up the self.relpath() functionality
 
467
    auth = config.AuthenticationConfig()
 
468
    if username is None:
 
469
        username = auth.get_user('ssh', host, port=port)
 
470
        if username is None:
 
471
            # Default to local user
 
472
            username = getpass.getuser()
 
473
 
 
474
    if _use_ssh_agent:
 
475
        agent = paramiko.Agent()
 
476
        for key in agent.get_keys():
 
477
            trace.mutter('Trying SSH agent key %s'
 
478
                         % paramiko.util.hexify(key.get_fingerprint()))
 
479
            try:
 
480
                paramiko_transport.auth_publickey(username, key)
 
481
                return
 
482
            except paramiko.SSHException, e:
 
483
                pass
 
484
 
 
485
    # okay, try finding id_rsa or id_dss?  (posix only)
 
486
    if _try_pkey_auth(paramiko_transport, paramiko.RSAKey, username, 'id_rsa'):
 
487
        return
 
488
    if _try_pkey_auth(paramiko_transport, paramiko.DSSKey, username, 'id_dsa'):
 
489
        return
 
490
 
 
491
    if password:
 
492
        try:
 
493
            paramiko_transport.auth_password(username, password)
 
494
            return
 
495
        except paramiko.SSHException, e:
 
496
            pass
 
497
 
 
498
    # give up and ask for a password
 
499
    password = auth.get_password('ssh', host, username, port=port)
 
500
    try:
 
501
        paramiko_transport.auth_password(username, password)
 
502
    except paramiko.SSHException, e:
 
503
        raise errors.ConnectionError(
 
504
            'Unable to authenticate to SSH host as %s@%s' % (username, host), e)
 
505
 
 
506
 
 
507
def _try_pkey_auth(paramiko_transport, pkey_class, username, filename):
 
508
    filename = os.path.expanduser('~/.ssh/' + filename)
 
509
    try:
 
510
        key = pkey_class.from_private_key_file(filename)
 
511
        paramiko_transport.auth_publickey(username, key)
 
512
        return True
 
513
    except paramiko.PasswordRequiredException:
 
514
        password = ui.ui_factory.get_password(
 
515
            prompt='SSH %(filename)s password', filename=filename)
 
516
        try:
 
517
            key = pkey_class.from_private_key_file(filename, password)
 
518
            paramiko_transport.auth_publickey(username, key)
 
519
            return True
 
520
        except paramiko.SSHException:
 
521
            trace.mutter('SSH authentication via %s key failed.'
 
522
                         % (os.path.basename(filename),))
 
523
    except paramiko.SSHException:
 
524
        trace.mutter('SSH authentication via %s key failed.'
 
525
                     % (os.path.basename(filename),))
 
526
    except IOError:
 
527
        pass
 
528
    return False
 
529
 
 
530
 
 
531
def load_host_keys():
 
532
    """
 
533
    Load system host keys (probably doesn't work on windows) and any
 
534
    "discovered" keys from previous sessions.
 
535
    """
 
536
    global SYSTEM_HOSTKEYS, BZR_HOSTKEYS
 
537
    try:
 
538
        SYSTEM_HOSTKEYS = paramiko.util.load_host_keys(
 
539
            os.path.expanduser('~/.ssh/known_hosts'))
 
540
    except IOError, e:
 
541
        trace.mutter('failed to load system host keys: ' + str(e))
 
542
    bzr_hostkey_path = osutils.pathjoin(config.config_dir(), 'ssh_host_keys')
 
543
    try:
 
544
        BZR_HOSTKEYS = paramiko.util.load_host_keys(bzr_hostkey_path)
 
545
    except IOError, e:
 
546
        trace.mutter('failed to load bzr host keys: ' + str(e))
 
547
        save_host_keys()
 
548
 
 
549
 
 
550
def save_host_keys():
 
551
    """
 
552
    Save "discovered" host keys in $(config)/ssh_host_keys/.
 
553
    """
 
554
    global SYSTEM_HOSTKEYS, BZR_HOSTKEYS
 
555
    bzr_hostkey_path = osutils.pathjoin(config.config_dir(), 'ssh_host_keys')
 
556
    config.ensure_config_dir_exists()
 
557
 
 
558
    try:
 
559
        f = open(bzr_hostkey_path, 'w')
 
560
        f.write('# SSH host keys collected by bzr\n')
 
561
        for hostname, keys in BZR_HOSTKEYS.iteritems():
 
562
            for keytype, key in keys.iteritems():
 
563
                f.write('%s %s %s\n' % (hostname, keytype, key.get_base64()))
 
564
        f.close()
 
565
    except IOError, e:
 
566
        trace.mutter('failed to save bzr host keys: ' + str(e))
 
567
 
 
568
 
 
569
def os_specific_subprocess_params():
 
570
    """Get O/S specific subprocess parameters."""
 
571
    if sys.platform == 'win32':
 
572
        # setting the process group and closing fds is not supported on 
 
573
        # win32
 
574
        return {}
 
575
    else:
 
576
        # We close fds other than the pipes as the child process does not need 
 
577
        # them to be open.
 
578
        #
 
579
        # We also set the child process to ignore SIGINT.  Normally the signal
 
580
        # would be sent to every process in the foreground process group, but
 
581
        # this causes it to be seen only by bzr and not by ssh.  Python will
 
582
        # generate a KeyboardInterrupt in bzr, and we will then have a chance
 
583
        # to release locks or do other cleanup over ssh before the connection
 
584
        # goes away.  
 
585
        # <https://launchpad.net/products/bzr/+bug/5987>
 
586
        #
 
587
        # Running it in a separate process group is not good because then it
 
588
        # can't get non-echoed input of a password or passphrase.
 
589
        # <https://launchpad.net/products/bzr/+bug/40508>
 
590
        return {'preexec_fn': _ignore_sigint,
 
591
                'close_fds': True,
 
592
                }
 
593
 
 
594
 
 
595
class SSHSubprocess(object):
 
596
    """A socket-like object that talks to an ssh subprocess via pipes."""
 
597
 
 
598
    def __init__(self, proc):
 
599
        self.proc = proc
 
600
 
 
601
    def send(self, data):
 
602
        return os.write(self.proc.stdin.fileno(), data)
 
603
 
 
604
    def recv_ready(self):
 
605
        # TODO: jam 20051215 this function is necessary to support the
 
606
        # pipelined() function. In reality, it probably should use
 
607
        # poll() or select() to actually return if there is data
 
608
        # available, otherwise we probably don't get any benefit
 
609
        return True
 
610
 
 
611
    def recv(self, count):
 
612
        return os.read(self.proc.stdout.fileno(), count)
 
613
 
 
614
    def close(self):
 
615
        self.proc.stdin.close()
 
616
        self.proc.stdout.close()
 
617
        self.proc.wait()
 
618
 
 
619
    def get_filelike_channels(self):
 
620
        return (self.proc.stdout, self.proc.stdin)
 
621