/brz/remove-bazaar

To get this branch, use:
bzr branch http://gegoxaren.bato24.eu/bzr/brz/remove-bazaar

« back to all changes in this revision

Viewing changes to bzrlib/transport/ssh.py

  • Committer: Robert Collins
  • Date: 2007-10-15 02:25:37 UTC
  • mto: This revision was merged to the branch mainline in revision 2907.
  • Revision ID: robertc@robertcollins.net-20071015022537-37pczn78yq27f8yg
* New method ``bzrlib.repository.Repository.is_write_locked`` useful for
  determining if a repository is write locked. (Robert Collins)

Show diffs side-by-side

added added

removed removed

Lines of Context:
1
 
# Copyright (C) 2006-2010 Robey Pointer <robey@lag.net>
 
1
# Copyright (C) 2005 Robey Pointer <robey@lag.net>
2
2
# Copyright (C) 2005, 2006, 2007 Canonical Ltd
3
3
#
4
4
# This program is free software; you can redistribute it and/or modify
13
13
#
14
14
# You should have received a copy of the GNU General Public License
15
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
 
16
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
17
17
 
18
18
"""Foundation SSH support for SFTP and smart server."""
19
19
 
20
20
import errno
21
21
import getpass
22
 
import logging
23
22
import os
24
23
import socket
25
24
import subprocess
26
25
import sys
27
26
 
28
 
from bzrlib import (
29
 
    config,
30
 
    errors,
31
 
    osutils,
32
 
    trace,
33
 
    ui,
34
 
    )
 
27
from bzrlib.config import config_dir, ensure_config_dir_exists
 
28
from bzrlib.errors import (ConnectionError,
 
29
                           ParamikoNotPresent,
 
30
                           SocketConnectionError,
 
31
                           SSHVendorNotFound,
 
32
                           TransportError,
 
33
                           UnknownSSH,
 
34
                           )
 
35
 
 
36
from bzrlib.osutils import pathjoin
 
37
from bzrlib.trace import mutter, warning
 
38
import bzrlib.ui
35
39
 
36
40
try:
37
41
    import paramiko
94
98
            try:
95
99
                vendor = self._ssh_vendors[vendor_name]
96
100
            except KeyError:
97
 
                vendor = self._get_vendor_from_path(vendor_name)
98
 
                if vendor is None:
99
 
                    raise errors.UnknownSSH(vendor_name)
100
 
                vendor.executable_path = vendor_name
 
101
                raise UnknownSSH(vendor_name)
101
102
            return vendor
102
103
        return None
103
104
 
113
114
            stdout = stderr = ''
114
115
        return stdout + stderr
115
116
 
116
 
    def _get_vendor_by_version_string(self, version, progname):
 
117
    def _get_vendor_by_version_string(self, version, args):
117
118
        """Return the vendor or None based on output from the subprocess.
118
119
 
119
120
        :param version: The output of 'ssh -V' like command.
121
122
        """
122
123
        vendor = None
123
124
        if 'OpenSSH' in version:
124
 
            trace.mutter('ssh implementation is OpenSSH')
 
125
            mutter('ssh implementation is OpenSSH')
125
126
            vendor = OpenSSHSubprocessVendor()
126
127
        elif 'SSH Secure Shell' in version:
127
 
            trace.mutter('ssh implementation is SSH Corp.')
 
128
            mutter('ssh implementation is SSH Corp.')
128
129
            vendor = SSHCorpSubprocessVendor()
129
 
        # As plink user prompts are not handled currently, don't auto-detect
130
 
        # it by inspection below, but keep this vendor detection for if a path
131
 
        # is given in BZR_SSH. See https://bugs.launchpad.net/bugs/414743
132
 
        elif 'plink' in version and progname == 'plink':
133
 
            # Checking if "plink" was the executed argument as Windows
134
 
            # sometimes reports 'ssh -V' incorrectly with 'plink' in it's
135
 
            # version.  See https://bugs.launchpad.net/bzr/+bug/107155
136
 
            trace.mutter("ssh implementation is Putty's plink.")
 
130
        elif 'plink' in version and args[0] == 'plink':
 
131
            # Checking if "plink" was the executed argument as Windows sometimes 
 
132
            # reports 'ssh -V' incorrectly with 'plink' in it's version. 
 
133
            # See https://bugs.launchpad.net/bzr/+bug/107155
 
134
            mutter("ssh implementation is Putty's plink.")
137
135
            vendor = PLinkSubprocessVendor()
138
136
        return vendor
139
137
 
140
138
    def _get_vendor_by_inspection(self):
141
139
        """Return the vendor or None by checking for known SSH implementations."""
142
 
        version = self._get_ssh_version_string(['ssh', '-V'])
143
 
        return self._get_vendor_by_version_string(version, "ssh")
144
 
 
145
 
    def _get_vendor_from_path(self, path):
146
 
        """Return the vendor or None using the program at the given path"""
147
 
        version = self._get_ssh_version_string([path, '-V'])
148
 
        return self._get_vendor_by_version_string(version, 
149
 
            os.path.splitext(os.path.basename(path))[0])
 
140
        for args in [['ssh', '-V'], ['plink', '-V']]:
 
141
            version = self._get_ssh_version_string(args)
 
142
            vendor = self._get_vendor_by_version_string(version, args)
 
143
            if vendor is not None:
 
144
                return vendor
 
145
        return None
150
146
 
151
147
    def get_vendor(self, environment=None):
152
148
        """Find out what version of SSH is on the system.
160
156
            if vendor is None:
161
157
                vendor = self._get_vendor_by_inspection()
162
158
                if vendor is None:
163
 
                    trace.mutter('falling back to default implementation')
 
159
                    mutter('falling back to default implementation')
164
160
                    vendor = self._default_ssh_vendor
165
161
                    if vendor is None:
166
 
                        raise errors.SSHVendorNotFound()
 
162
                        raise SSHVendorNotFound()
167
163
            self._cached_ssh_vendor = vendor
168
164
        return self._cached_ssh_vendor
169
165
 
173
169
register_ssh_vendor = _ssh_vendor_manager.register_vendor
174
170
 
175
171
 
176
 
def _ignore_signals():
 
172
def _ignore_sigint():
177
173
    # TODO: This should possibly ignore SIGHUP as well, but bzr currently
178
174
    # doesn't handle it itself.
179
175
    # <https://launchpad.net/products/bzr/+bug/41433/+index>
180
176
    import signal
181
177
    signal.signal(signal.SIGINT, signal.SIG_IGN)
182
 
    # GZ 2010-02-19: Perhaps make this check if breakin is installed instead
183
 
    if signal.getsignal(signal.SIGQUIT) != signal.SIG_DFL:
184
 
        signal.signal(signal.SIGQUIT, signal.SIG_IGN)
185
 
 
186
 
 
187
 
class SocketAsChannelAdapter(object):
 
178
 
 
179
 
 
180
class LoopbackSFTP(object):
188
181
    """Simple wrapper for a socket that pretends to be a paramiko Channel."""
189
182
 
190
183
    def __init__(self, sock):
191
184
        self.__socket = sock
192
 
 
193
 
    def get_name(self):
194
 
        return "bzr SocketAsChannelAdapter"
195
 
 
 
185
 
196
186
    def send(self, data):
197
187
        return self.__socket.send(data)
198
188
 
199
189
    def recv(self, n):
200
 
        try:
201
 
            return self.__socket.recv(n)
202
 
        except socket.error, e:
203
 
            if e.args[0] in (errno.EPIPE, errno.ECONNRESET, errno.ECONNABORTED,
204
 
                             errno.EBADF):
205
 
                # Connection has closed.  Paramiko expects an empty string in
206
 
                # this case, not an exception.
207
 
                return ''
208
 
            raise
 
190
        return self.__socket.recv(n)
209
191
 
210
192
    def recv_ready(self):
211
 
        # TODO: jam 20051215 this function is necessary to support the
212
 
        # pipelined() function. In reality, it probably should use
213
 
        # poll() or select() to actually return if there is data
214
 
        # available, otherwise we probably don't get any benefit
215
193
        return True
216
194
 
217
195
    def close(self):
220
198
 
221
199
class SSHVendor(object):
222
200
    """Abstract base class for SSH vendor implementations."""
223
 
 
 
201
    
224
202
    def connect_sftp(self, username, password, host, port):
225
203
        """Make an SSH connection, and return an SFTPClient.
226
 
 
 
204
        
227
205
        :param username: an ascii string
228
206
        :param password: an ascii string
229
207
        :param host: a host name as an ascii string
238
216
 
239
217
    def connect_ssh(self, username, password, host, port, command):
240
218
        """Make an SSH connection.
241
 
 
 
219
        
242
220
        :returns: something with a `close` method, and a `get_filelike_channels`
243
221
            method that returns a pair of (read, write) filelike objects.
244
222
        """
245
223
        raise NotImplementedError(self.connect_ssh)
246
 
 
 
224
        
247
225
    def _raise_connection_error(self, host, port=None, orig_error=None,
248
226
                                msg='Unable to connect to SSH host'):
249
227
        """Raise a SocketConnectionError with properly formatted host.
251
229
        This just unifies all the locations that try to raise ConnectionError,
252
230
        so that they format things properly.
253
231
        """
254
 
        raise errors.SocketConnectionError(host=host, port=port, msg=msg,
255
 
                                           orig_error=orig_error)
 
232
        raise SocketConnectionError(host=host, port=port, msg=msg,
 
233
                                    orig_error=orig_error)
256
234
 
257
235
 
258
236
class LoopbackVendor(SSHVendor):
259
237
    """SSH "vendor" that connects over a plain TCP socket, not SSH."""
260
 
 
 
238
    
261
239
    def connect_sftp(self, username, password, host, port):
262
240
        sock = socket.socket()
263
241
        try:
264
242
            sock.connect((host, port))
265
243
        except socket.error, e:
266
244
            self._raise_connection_error(host, port=port, orig_error=e)
267
 
        return SFTPClient(SocketAsChannelAdapter(sock))
 
245
        return SFTPClient(LoopbackSFTP(sock))
268
246
 
269
247
register_ssh_vendor('loopback', LoopbackVendor())
270
248
 
285
263
 
286
264
    def _connect(self, username, password, host, port):
287
265
        global SYSTEM_HOSTKEYS, BZR_HOSTKEYS
288
 
 
 
266
        
289
267
        load_host_keys()
290
268
 
291
269
        try:
294
272
            t.start_client()
295
273
        except (paramiko.SSHException, socket.error), e:
296
274
            self._raise_connection_error(host, port=port, orig_error=e)
297
 
 
 
275
            
298
276
        server_key = t.get_remote_server_key()
299
277
        server_key_hex = paramiko.util.hexify(server_key.get_fingerprint())
300
278
        keytype = server_key.get_name()
301
279
        if host in SYSTEM_HOSTKEYS and keytype in SYSTEM_HOSTKEYS[host]:
302
280
            our_server_key = SYSTEM_HOSTKEYS[host][keytype]
303
 
            our_server_key_hex = paramiko.util.hexify(
304
 
                our_server_key.get_fingerprint())
 
281
            our_server_key_hex = paramiko.util.hexify(our_server_key.get_fingerprint())
305
282
        elif host in BZR_HOSTKEYS and keytype in BZR_HOSTKEYS[host]:
306
283
            our_server_key = BZR_HOSTKEYS[host][keytype]
307
 
            our_server_key_hex = paramiko.util.hexify(
308
 
                our_server_key.get_fingerprint())
 
284
            our_server_key_hex = paramiko.util.hexify(our_server_key.get_fingerprint())
309
285
        else:
310
 
            trace.warning('Adding %s host key for %s: %s'
311
 
                          % (keytype, host, server_key_hex))
 
286
            warning('Adding %s host key for %s: %s' % (keytype, host, server_key_hex))
312
287
            add = getattr(BZR_HOSTKEYS, 'add', None)
313
288
            if add is not None: # paramiko >= 1.X.X
314
289
                BZR_HOSTKEYS.add(host, keytype, server_key)
315
290
            else:
316
291
                BZR_HOSTKEYS.setdefault(host, {})[keytype] = server_key
317
292
            our_server_key = server_key
318
 
            our_server_key_hex = paramiko.util.hexify(
319
 
                our_server_key.get_fingerprint())
 
293
            our_server_key_hex = paramiko.util.hexify(our_server_key.get_fingerprint())
320
294
            save_host_keys()
321
295
        if server_key != our_server_key:
322
296
            filename1 = os.path.expanduser('~/.ssh/known_hosts')
323
 
            filename2 = osutils.pathjoin(config.config_dir(), 'ssh_host_keys')
324
 
            raise errors.TransportError(
325
 
                'Host keys for %s do not match!  %s != %s' %
 
297
            filename2 = pathjoin(config_dir(), 'ssh_host_keys')
 
298
            raise TransportError('Host keys for %s do not match!  %s != %s' % \
326
299
                (host, our_server_key_hex, server_key_hex),
327
300
                ['Try editing %s or %s' % (filename1, filename2)])
328
301
 
329
 
        _paramiko_auth(username, password, host, port, t)
 
302
        _paramiko_auth(username, password, host, t)
330
303
        return t
331
 
 
 
304
        
332
305
    def connect_sftp(self, username, password, host, port):
333
306
        t = self._connect(username, password, host, port)
334
307
        try:
353
326
    register_ssh_vendor('paramiko', vendor)
354
327
    register_ssh_vendor('none', vendor)
355
328
    register_default_ssh_vendor(vendor)
356
 
    _sftp_connection_errors = (EOFError, paramiko.SSHException)
357
329
    del vendor
358
 
else:
359
 
    _sftp_connection_errors = (EOFError,)
360
330
 
361
331
 
362
332
class SubprocessVendor(SSHVendor):
363
333
    """Abstract base class for vendors that use pipes to a subprocess."""
364
 
 
 
334
    
365
335
    def _connect(self, argv):
366
336
        proc = subprocess.Popen(argv,
367
337
                                stdin=subprocess.PIPE,
374
344
            argv = self._get_vendor_specific_argv(username, host, port,
375
345
                                                  subsystem='sftp')
376
346
            sock = self._connect(argv)
377
 
            return SFTPClient(SocketAsChannelAdapter(sock))
378
 
        except _sftp_connection_errors, e:
 
347
            return SFTPClient(sock)
 
348
        except (EOFError, paramiko.SSHException), e:
379
349
            self._raise_connection_error(host, port=port, orig_error=e)
380
350
        except (OSError, IOError), e:
381
351
            # If the machine is fast enough, ssh can actually exit
403
373
    def _get_vendor_specific_argv(self, username, host, port, subsystem=None,
404
374
                                  command=None):
405
375
        """Returns the argument list to run the subprocess with.
406
 
 
 
376
        
407
377
        Exactly one of 'subsystem' and 'command' must be specified.
408
378
        """
409
379
        raise NotImplementedError(self._get_vendor_specific_argv)
411
381
 
412
382
class OpenSSHSubprocessVendor(SubprocessVendor):
413
383
    """SSH vendor that uses the 'ssh' executable from OpenSSH."""
414
 
 
415
 
    executable_path = 'ssh'
416
 
 
 
384
    
417
385
    def _get_vendor_specific_argv(self, username, host, port, subsystem=None,
418
386
                                  command=None):
419
 
        args = [self.executable_path,
 
387
        assert subsystem is not None or command is not None, (
 
388
            'Must specify a command or subsystem')
 
389
        if subsystem is not None:
 
390
            assert command is None, (
 
391
                'subsystem and command are mutually exclusive')
 
392
        args = ['ssh',
420
393
                '-oForwardX11=no', '-oForwardAgent=no',
421
394
                '-oClearAllForwardings=yes', '-oProtocol=2',
422
395
                '-oNoHostAuthenticationForLocalhost=yes']
436
409
class SSHCorpSubprocessVendor(SubprocessVendor):
437
410
    """SSH vendor that uses the 'ssh' executable from SSH Corporation."""
438
411
 
439
 
    executable_path = 'ssh'
440
 
 
441
412
    def _get_vendor_specific_argv(self, username, host, port, subsystem=None,
442
413
                                  command=None):
443
 
        args = [self.executable_path, '-x']
 
414
        assert subsystem is not None or command is not None, (
 
415
            'Must specify a command or subsystem')
 
416
        if subsystem is not None:
 
417
            assert command is None, (
 
418
                'subsystem and command are mutually exclusive')
 
419
        args = ['ssh', '-x']
444
420
        if port is not None:
445
421
            args.extend(['-p', str(port)])
446
422
        if username is not None:
450
426
        else:
451
427
            args.extend([host] + command)
452
428
        return args
453
 
 
454
 
register_ssh_vendor('sshcorp', SSHCorpSubprocessVendor())
 
429
    
 
430
register_ssh_vendor('ssh', SSHCorpSubprocessVendor())
455
431
 
456
432
 
457
433
class PLinkSubprocessVendor(SubprocessVendor):
458
434
    """SSH vendor that uses the 'plink' executable from Putty."""
459
435
 
460
 
    executable_path = 'plink'
461
 
 
462
436
    def _get_vendor_specific_argv(self, username, host, port, subsystem=None,
463
437
                                  command=None):
464
 
        args = [self.executable_path, '-x', '-a', '-ssh', '-2', '-batch']
 
438
        assert subsystem is not None or command is not None, (
 
439
            'Must specify a command or subsystem')
 
440
        if subsystem is not None:
 
441
            assert command is None, (
 
442
                'subsystem and command are mutually exclusive')
 
443
        args = ['plink', '-x', '-a', '-ssh', '-2']
465
444
        if port is not None:
466
445
            args.extend(['-P', str(port)])
467
446
        if username is not None:
475
454
register_ssh_vendor('plink', PLinkSubprocessVendor())
476
455
 
477
456
 
478
 
def _paramiko_auth(username, password, host, port, paramiko_transport):
479
 
    auth = config.AuthenticationConfig()
480
 
    # paramiko requires a username, but it might be none if nothing was
481
 
    # supplied.  If so, use the local username.
482
 
    if username is None:
483
 
        username = auth.get_user('ssh', host, port=port,
484
 
                                 default=getpass.getuser())
 
457
def _paramiko_auth(username, password, host, paramiko_transport):
 
458
    # paramiko requires a username, but it might be none if nothing was supplied
 
459
    # use the local username, just in case.
 
460
    # We don't override username, because if we aren't using paramiko,
 
461
    # the username might be specified in ~/.ssh/config and we don't want to
 
462
    # force it to something else
 
463
    # Also, it would mess up the self.relpath() functionality
 
464
    username = username or getpass.getuser()
 
465
 
485
466
    if _use_ssh_agent:
486
467
        agent = paramiko.Agent()
487
468
        for key in agent.get_keys():
488
 
            trace.mutter('Trying SSH agent key %s'
489
 
                         % paramiko.util.hexify(key.get_fingerprint()))
 
469
            mutter('Trying SSH agent key %s' % paramiko.util.hexify(key.get_fingerprint()))
490
470
            try:
491
471
                paramiko_transport.auth_publickey(username, key)
492
472
                return
493
473
            except paramiko.SSHException, e:
494
474
                pass
495
 
 
 
475
    
496
476
    # okay, try finding id_rsa or id_dss?  (posix only)
497
477
    if _try_pkey_auth(paramiko_transport, paramiko.RSAKey, username, 'id_rsa'):
498
478
        return
499
479
    if _try_pkey_auth(paramiko_transport, paramiko.DSSKey, username, 'id_dsa'):
500
480
        return
501
481
 
502
 
    # If we have gotten this far, we are about to try for passwords, do an
503
 
    # auth_none check to see if it is even supported.
504
 
    supported_auth_types = []
505
 
    try:
506
 
        # Note that with paramiko <1.7.5 this logs an INFO message:
507
 
        #    Authentication type (none) not permitted.
508
 
        # So we explicitly disable the logging level for this action
509
 
        old_level = paramiko_transport.logger.level
510
 
        paramiko_transport.logger.setLevel(logging.WARNING)
511
 
        try:
512
 
            paramiko_transport.auth_none(username)
513
 
        finally:
514
 
            paramiko_transport.logger.setLevel(old_level)
515
 
    except paramiko.BadAuthenticationType, e:
516
 
        # Supported methods are in the exception
517
 
        supported_auth_types = e.allowed_types
518
 
    except paramiko.SSHException, e:
519
 
        # Don't know what happened, but just ignore it
520
 
        pass
521
 
    # We treat 'keyboard-interactive' and 'password' auth methods identically,
522
 
    # because Paramiko's auth_password method will automatically try
523
 
    # 'keyboard-interactive' auth (using the password as the response) if
524
 
    # 'password' auth is not available.  Apparently some Debian and Gentoo
525
 
    # OpenSSH servers require this.
526
 
    # XXX: It's possible for a server to require keyboard-interactive auth that
527
 
    # requires something other than a single password, but we currently don't
528
 
    # support that.
529
 
    if ('password' not in supported_auth_types and
530
 
        'keyboard-interactive' not in supported_auth_types):
531
 
        raise errors.ConnectionError('Unable to authenticate to SSH host as'
532
 
            '\n  %s@%s\nsupported auth types: %s'
533
 
            % (username, host, supported_auth_types))
534
 
 
535
482
    if password:
536
483
        try:
537
484
            paramiko_transport.auth_password(username, password)
540
487
            pass
541
488
 
542
489
    # give up and ask for a password
543
 
    password = auth.get_password('ssh', host, username, port=port)
544
 
    # get_password can still return None, which means we should not prompt
545
 
    if password is not None:
546
 
        try:
547
 
            paramiko_transport.auth_password(username, password)
548
 
        except paramiko.SSHException, e:
549
 
            raise errors.ConnectionError(
550
 
                'Unable to authenticate to SSH host as'
551
 
                '\n  %s@%s\n' % (username, host), e)
552
 
    else:
553
 
        raise errors.ConnectionError('Unable to authenticate to SSH host as'
554
 
                                     '  %s@%s' % (username, host))
 
490
    password = bzrlib.ui.ui_factory.get_password(
 
491
            prompt='SSH %(user)s@%(host)s password',
 
492
            user=username, host=host)
 
493
    try:
 
494
        paramiko_transport.auth_password(username, password)
 
495
    except paramiko.SSHException, e:
 
496
        raise ConnectionError('Unable to authenticate to SSH host as %s@%s' %
 
497
                              (username, host), e)
555
498
 
556
499
 
557
500
def _try_pkey_auth(paramiko_transport, pkey_class, username, filename):
561
504
        paramiko_transport.auth_publickey(username, key)
562
505
        return True
563
506
    except paramiko.PasswordRequiredException:
564
 
        password = ui.ui_factory.get_password(
565
 
            prompt='SSH %(filename)s password', filename=filename)
 
507
        password = bzrlib.ui.ui_factory.get_password(
 
508
                prompt='SSH %(filename)s password',
 
509
                filename=filename)
566
510
        try:
567
511
            key = pkey_class.from_private_key_file(filename, password)
568
512
            paramiko_transport.auth_publickey(username, key)
569
513
            return True
570
514
        except paramiko.SSHException:
571
 
            trace.mutter('SSH authentication via %s key failed.'
572
 
                         % (os.path.basename(filename),))
 
515
            mutter('SSH authentication via %s key failed.' % (os.path.basename(filename),))
573
516
    except paramiko.SSHException:
574
 
        trace.mutter('SSH authentication via %s key failed.'
575
 
                     % (os.path.basename(filename),))
 
517
        mutter('SSH authentication via %s key failed.' % (os.path.basename(filename),))
576
518
    except IOError:
577
519
        pass
578
520
    return False
585
527
    """
586
528
    global SYSTEM_HOSTKEYS, BZR_HOSTKEYS
587
529
    try:
588
 
        SYSTEM_HOSTKEYS = paramiko.util.load_host_keys(
589
 
            os.path.expanduser('~/.ssh/known_hosts'))
 
530
        SYSTEM_HOSTKEYS = paramiko.util.load_host_keys(os.path.expanduser('~/.ssh/known_hosts'))
590
531
    except IOError, e:
591
 
        trace.mutter('failed to load system host keys: ' + str(e))
592
 
    bzr_hostkey_path = osutils.pathjoin(config.config_dir(), 'ssh_host_keys')
 
532
        mutter('failed to load system host keys: ' + str(e))
 
533
    bzr_hostkey_path = pathjoin(config_dir(), 'ssh_host_keys')
593
534
    try:
594
535
        BZR_HOSTKEYS = paramiko.util.load_host_keys(bzr_hostkey_path)
595
536
    except IOError, e:
596
 
        trace.mutter('failed to load bzr host keys: ' + str(e))
 
537
        mutter('failed to load bzr host keys: ' + str(e))
597
538
        save_host_keys()
598
539
 
599
540
 
602
543
    Save "discovered" host keys in $(config)/ssh_host_keys/.
603
544
    """
604
545
    global SYSTEM_HOSTKEYS, BZR_HOSTKEYS
605
 
    bzr_hostkey_path = osutils.pathjoin(config.config_dir(), 'ssh_host_keys')
606
 
    config.ensure_config_dir_exists()
 
546
    bzr_hostkey_path = pathjoin(config_dir(), 'ssh_host_keys')
 
547
    ensure_config_dir_exists()
607
548
 
608
549
    try:
609
550
        f = open(bzr_hostkey_path, 'w')
613
554
                f.write('%s %s %s\n' % (hostname, keytype, key.get_base64()))
614
555
        f.close()
615
556
    except IOError, e:
616
 
        trace.mutter('failed to save bzr host keys: ' + str(e))
 
557
        mutter('failed to save bzr host keys: ' + str(e))
617
558
 
618
559
 
619
560
def os_specific_subprocess_params():
620
561
    """Get O/S specific subprocess parameters."""
621
562
    if sys.platform == 'win32':
622
 
        # setting the process group and closing fds is not supported on
 
563
        # setting the process group and closing fds is not supported on 
623
564
        # win32
624
565
        return {}
625
566
    else:
626
 
        # We close fds other than the pipes as the child process does not need
 
567
        # We close fds other than the pipes as the child process does not need 
627
568
        # them to be open.
628
569
        #
629
570
        # We also set the child process to ignore SIGINT.  Normally the signal
631
572
        # this causes it to be seen only by bzr and not by ssh.  Python will
632
573
        # generate a KeyboardInterrupt in bzr, and we will then have a chance
633
574
        # to release locks or do other cleanup over ssh before the connection
634
 
        # goes away.
 
575
        # goes away.  
635
576
        # <https://launchpad.net/products/bzr/+bug/5987>
636
577
        #
637
578
        # Running it in a separate process group is not good because then it
638
579
        # can't get non-echoed input of a password or passphrase.
639
580
        # <https://launchpad.net/products/bzr/+bug/40508>
640
 
        return {'preexec_fn': _ignore_signals,
 
581
        return {'preexec_fn': _ignore_sigint,
641
582
                'close_fds': True,
642
583
                }
643
584
 
644
 
import weakref
645
 
_subproc_weakrefs = set()
646
 
 
647
 
def _close_ssh_proc(proc):
648
 
    for func in [proc.stdin.close, proc.stdout.close, proc.wait]:
649
 
        try:
650
 
            func()
651
 
        except OSError:
652
 
            pass
653
 
 
654
585
 
655
586
class SSHSubprocess(object):
656
587
    """A socket-like object that talks to an ssh subprocess via pipes."""
657
588
 
658
589
    def __init__(self, proc):
659
590
        self.proc = proc
660
 
        # Add a weakref to proc that will attempt to do the same as self.close
661
 
        # to avoid leaving processes lingering indefinitely.
662
 
        def terminate(ref):
663
 
            _subproc_weakrefs.remove(ref)
664
 
            _close_ssh_proc(proc)
665
 
        _subproc_weakrefs.add(weakref.ref(self, terminate))
666
591
 
667
592
    def send(self, data):
668
593
        return os.write(self.proc.stdin.fileno(), data)
669
594
 
 
595
    def recv_ready(self):
 
596
        # TODO: jam 20051215 this function is necessary to support the
 
597
        # pipelined() function. In reality, it probably should use
 
598
        # poll() or select() to actually return if there is data
 
599
        # available, otherwise we probably don't get any benefit
 
600
        return True
 
601
 
670
602
    def recv(self, count):
671
603
        return os.read(self.proc.stdout.fileno(), count)
672
604
 
673
605
    def close(self):
674
 
        _close_ssh_proc(self.proc)
 
606
        self.proc.stdin.close()
 
607
        self.proc.stdout.close()
 
608
        self.proc.wait()
675
609
 
676
610
    def get_filelike_channels(self):
677
611
        return (self.proc.stdout, self.proc.stdin)