~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

Viewing changes to bzrlib/transport/ssh.py

  • Committer: Canonical.com Patch Queue Manager
  • Date: 2009-07-30 14:24:06 UTC
  • mfrom: (4576.1.1 export-to-dir)
  • Revision ID: pqm@pqm.ubuntu.com-20090730142406-wg8gmxpcjz4c1z00
(bialix) Allow 'bzr export' to export into an existing (but empty)
        directory

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