/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: 2010-05-06 23:41:35 UTC
  • mto: This revision was merged to the branch mainline in revision 5223.
  • Revision ID: robertc@robertcollins.net-20100506234135-yivbzczw1sejxnxc
Lock methods on ``Tree``, ``Branch`` and ``Repository`` are now
expected to return an object which can be used to unlock them. This reduces
duplicate code when using cleanups. The previous 'tokens's returned by
``Branch.lock_write`` and ``Repository.lock_write`` are now attributes
on the result of the lock_write. ``repository.RepositoryWriteLockResult``
and ``branch.BranchWriteLockResult`` document this. (Robert Collins)

``log._get_info_for_log_files`` now takes an add_cleanup callable.
(Robert Collins)

Show diffs side-by-side

added added

removed removed

Lines of Context:
1
 
# Copyright (C) 2006-2011 Robey Pointer <robey@lag.net>
 
1
# Copyright (C) 2006-2010 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
17
17
 
18
18
"""Foundation SSH support for SFTP and smart server."""
19
19
 
20
 
from __future__ import absolute_import
21
 
 
22
20
import errno
23
21
import getpass
24
22
import logging
26
24
import socket
27
25
import subprocess
28
26
import sys
29
 
from binascii import hexlify
30
27
 
31
 
from .. import (
 
28
from bzrlib import (
32
29
    config,
33
 
    bedding,
34
30
    errors,
35
31
    osutils,
36
32
    trace,
39
35
 
40
36
try:
41
37
    import paramiko
42
 
except ImportError as e:
 
38
except ImportError, e:
43
39
    # If we have an ssh subprocess, we don't strictly need paramiko for all ssh
44
40
    # access
45
41
    paramiko = None
47
43
    from paramiko.sftp_client import SFTPClient
48
44
 
49
45
 
50
 
class StrangeHostname(errors.BzrError):
51
 
    _fmt = "Refusing to connect to strange SSH hostname %(hostname)s"
52
 
 
53
 
 
54
46
SYSTEM_HOSTKEYS = {}
55
 
BRZ_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))
56
57
 
57
58
 
58
59
class SSHVendorManager(object):
59
60
    """Manager for manage SSH vendors."""
60
61
 
61
62
    # Note, although at first sign the class interface seems similar to
62
 
    # breezy.registry.Registry it is not possible/convenient to directly use
 
63
    # bzrlib.registry.Registry it is not possible/convenient to directly use
63
64
    # the Registry because the class just has "get()" interface instead of the
64
65
    # Registry's "get(key)".
65
66
 
80
81
        """Clear previously cached lookup result."""
81
82
        self._cached_ssh_vendor = None
82
83
 
83
 
    def _get_vendor_by_config(self):
84
 
        vendor_name = config.GlobalStack().get('ssh')
85
 
        if vendor_name is not None:
 
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']
86
94
            try:
87
95
                vendor = self._ssh_vendors[vendor_name]
88
96
            except KeyError:
99
107
            p = subprocess.Popen(args,
100
108
                                 stdout=subprocess.PIPE,
101
109
                                 stderr=subprocess.PIPE,
102
 
                                 bufsize=0,
103
110
                                 **os_specific_subprocess_params())
104
111
            stdout, stderr = p.communicate()
105
112
        except OSError:
106
 
            stdout = stderr = b''
107
 
        return (stdout + stderr).decode(osutils.get_terminal_encoding())
 
113
            stdout = stderr = ''
 
114
        return stdout + stderr
108
115
 
109
116
    def _get_vendor_by_version_string(self, version, progname):
110
117
        """Return the vendor or None based on output from the subprocess.
119
126
        elif 'SSH Secure Shell' in version:
120
127
            trace.mutter('ssh implementation is SSH Corp.')
121
128
            vendor = SSHCorpSubprocessVendor()
122
 
        elif 'lsh' in version:
123
 
            trace.mutter('ssh implementation is GNU lsh.')
124
 
            vendor = LSHSubprocessVendor()
125
129
        # As plink user prompts are not handled currently, don't auto-detect
126
130
        # it by inspection below, but keep this vendor detection for if a path
127
 
        # is given in BRZ_SSH. See https://bugs.launchpad.net/bugs/414743
 
131
        # is given in BZR_SSH. See https://bugs.launchpad.net/bugs/414743
128
132
        elif 'plink' in version and progname == 'plink':
129
133
            # Checking if "plink" was the executed argument as Windows
130
 
            # sometimes reports 'ssh -V' incorrectly with 'plink' in its
 
134
            # sometimes reports 'ssh -V' incorrectly with 'plink' in it's
131
135
            # version.  See https://bugs.launchpad.net/bzr/+bug/107155
132
136
            trace.mutter("ssh implementation is Putty's plink.")
133
137
            vendor = PLinkSubprocessVendor()
141
145
    def _get_vendor_from_path(self, path):
142
146
        """Return the vendor or None using the program at the given path"""
143
147
        version = self._get_ssh_version_string([path, '-V'])
144
 
        return self._get_vendor_by_version_string(version,
145
 
                                                  os.path.splitext(os.path.basename(path))[0])
 
148
        return self._get_vendor_by_version_string(version, 
 
149
            os.path.splitext(os.path.basename(path))[0])
146
150
 
147
 
    def get_vendor(self):
 
151
    def get_vendor(self, environment=None):
148
152
        """Find out what version of SSH is on the system.
149
153
 
150
154
        :raises SSHVendorNotFound: if no any SSH vendor is found
151
 
        :raises UnknownSSH: if the BRZ_SSH environment variable contains
 
155
        :raises UnknownSSH: if the BZR_SSH environment variable contains
152
156
                            unknown vendor name
153
157
        """
154
158
        if self._cached_ssh_vendor is None:
155
 
            vendor = self._get_vendor_by_config()
 
159
            vendor = self._get_vendor_by_environment(environment)
156
160
            if vendor is None:
157
161
                vendor = self._get_vendor_by_inspection()
158
162
                if vendor is None:
163
167
            self._cached_ssh_vendor = vendor
164
168
        return self._cached_ssh_vendor
165
169
 
166
 
 
167
170
_ssh_vendor_manager = SSHVendorManager()
168
171
_get_ssh_vendor = _ssh_vendor_manager.get_vendor
169
172
register_default_ssh_vendor = _ssh_vendor_manager.register_default_vendor
196
199
    def recv(self, n):
197
200
        try:
198
201
            return self.__socket.recv(n)
199
 
        except socket.error as e:
 
202
        except socket.error, e:
200
203
            if e.args[0] in (errno.EPIPE, errno.ECONNRESET, errno.ECONNABORTED,
201
204
                             errno.EBADF):
202
205
                # Connection has closed.  Paramiko expects an empty string in
236
239
    def connect_ssh(self, username, password, host, port, command):
237
240
        """Make an SSH connection.
238
241
 
239
 
        :returns: an SSHConnection.
 
242
        :returns: something with a `close` method, and a `get_filelike_channels`
 
243
            method that returns a pair of (read, write) filelike objects.
240
244
        """
241
245
        raise NotImplementedError(self.connect_ssh)
242
246
 
258
262
        sock = socket.socket()
259
263
        try:
260
264
            sock.connect((host, port))
261
 
        except socket.error as e:
 
265
        except socket.error, e:
262
266
            self._raise_connection_error(host, port=port, orig_error=e)
263
267
        return SFTPClient(SocketAsChannelAdapter(sock))
264
268
 
265
 
 
266
269
register_ssh_vendor('loopback', LoopbackVendor())
267
270
 
268
271
 
 
272
class _ParamikoSSHConnection(object):
 
273
    def __init__(self, channel):
 
274
        self.channel = channel
 
275
 
 
276
    def get_filelike_channels(self):
 
277
        return self.channel.makefile('rb'), self.channel.makefile('wb')
 
278
 
 
279
    def close(self):
 
280
        return self.channel.close()
 
281
 
 
282
 
269
283
class ParamikoVendor(SSHVendor):
270
284
    """Vendor that uses paramiko."""
271
285
 
272
 
    def _hexify(self, s):
273
 
        return hexlify(s).upper()
274
 
 
275
286
    def _connect(self, username, password, host, port):
276
 
        global SYSTEM_HOSTKEYS, BRZ_HOSTKEYS
 
287
        global SYSTEM_HOSTKEYS, BZR_HOSTKEYS
277
288
 
278
289
        load_host_keys()
279
290
 
281
292
            t = paramiko.Transport((host, port or 22))
282
293
            t.set_log_channel('bzr.paramiko')
283
294
            t.start_client()
284
 
        except (paramiko.SSHException, socket.error) as e:
 
295
        except (paramiko.SSHException, socket.error), e:
285
296
            self._raise_connection_error(host, port=port, orig_error=e)
286
297
 
287
298
        server_key = t.get_remote_server_key()
288
 
        server_key_hex = self._hexify(server_key.get_fingerprint())
 
299
        server_key_hex = paramiko.util.hexify(server_key.get_fingerprint())
289
300
        keytype = server_key.get_name()
290
301
        if host in SYSTEM_HOSTKEYS and keytype in SYSTEM_HOSTKEYS[host]:
291
302
            our_server_key = SYSTEM_HOSTKEYS[host][keytype]
292
 
            our_server_key_hex = self._hexify(our_server_key.get_fingerprint())
293
 
        elif host in BRZ_HOSTKEYS and keytype in BRZ_HOSTKEYS[host]:
294
 
            our_server_key = BRZ_HOSTKEYS[host][keytype]
295
 
            our_server_key_hex = self._hexify(our_server_key.get_fingerprint())
 
303
            our_server_key_hex = paramiko.util.hexify(
 
304
                our_server_key.get_fingerprint())
 
305
        elif host in BZR_HOSTKEYS and keytype in BZR_HOSTKEYS[host]:
 
306
            our_server_key = BZR_HOSTKEYS[host][keytype]
 
307
            our_server_key_hex = paramiko.util.hexify(
 
308
                our_server_key.get_fingerprint())
296
309
        else:
297
310
            trace.warning('Adding %s host key for %s: %s'
298
311
                          % (keytype, host, server_key_hex))
299
 
            add = getattr(BRZ_HOSTKEYS, 'add', None)
300
 
            if add is not None:  # paramiko >= 1.X.X
301
 
                BRZ_HOSTKEYS.add(host, keytype, server_key)
 
312
            add = getattr(BZR_HOSTKEYS, 'add', None)
 
313
            if add is not None: # paramiko >= 1.X.X
 
314
                BZR_HOSTKEYS.add(host, keytype, server_key)
302
315
            else:
303
 
                BRZ_HOSTKEYS.setdefault(host, {})[keytype] = server_key
 
316
                BZR_HOSTKEYS.setdefault(host, {})[keytype] = server_key
304
317
            our_server_key = server_key
305
 
            our_server_key_hex = self._hexify(our_server_key.get_fingerprint())
 
318
            our_server_key_hex = paramiko.util.hexify(
 
319
                our_server_key.get_fingerprint())
306
320
            save_host_keys()
307
321
        if server_key != our_server_key:
308
322
            filename1 = os.path.expanduser('~/.ssh/known_hosts')
309
 
            filename2 = _ssh_host_keys_config_dir()
 
323
            filename2 = osutils.pathjoin(config.config_dir(), 'ssh_host_keys')
310
324
            raise errors.TransportError(
311
325
                'Host keys for %s do not match!  %s != %s' %
312
326
                (host, our_server_key_hex, server_key_hex),
319
333
        t = self._connect(username, password, host, port)
320
334
        try:
321
335
            return t.open_sftp_client()
322
 
        except paramiko.SSHException as e:
 
336
        except paramiko.SSHException, e:
323
337
            self._raise_connection_error(host, port=port, orig_error=e,
324
338
                                         msg='Unable to start sftp client')
325
339
 
330
344
            cmdline = ' '.join(command)
331
345
            channel.exec_command(cmdline)
332
346
            return _ParamikoSSHConnection(channel)
333
 
        except paramiko.SSHException as e:
 
347
        except paramiko.SSHException, e:
334
348
            self._raise_connection_error(host, port=port, orig_error=e,
335
349
                                         msg='Unable to invoke remote bzr')
336
350
 
337
 
 
338
 
_ssh_connection_errors = (EOFError, OSError, IOError, socket.error)
339
351
if paramiko is not None:
340
352
    vendor = ParamikoVendor()
341
353
    register_ssh_vendor('paramiko', vendor)
342
354
    register_ssh_vendor('none', vendor)
343
355
    register_default_ssh_vendor(vendor)
344
 
    _ssh_connection_errors += (paramiko.SSHException,)
 
356
    _sftp_connection_errors = (EOFError, paramiko.SSHException)
345
357
    del vendor
 
358
else:
 
359
    _sftp_connection_errors = (EOFError,)
346
360
 
347
361
 
348
362
class SubprocessVendor(SSHVendor):
349
363
    """Abstract base class for vendors that use pipes to a subprocess."""
350
364
 
351
 
    # In general stderr should be inherited from the parent process so prompts
352
 
    # are visible on the terminal. This can be overriden to another file for
353
 
    # tests, but beware of using PIPE which may hang due to not being read.
354
 
    _stderr_target = None
355
 
 
356
 
    @staticmethod
357
 
    def _check_hostname(arg):
358
 
        if arg.startswith('-'):
359
 
            raise StrangeHostname(hostname=arg)
360
 
 
361
365
    def _connect(self, argv):
362
 
        # Attempt to make a socketpair to use as stdin/stdout for the SSH
363
 
        # subprocess.  We prefer sockets to pipes because they support
364
 
        # non-blocking short reads, allowing us to optimistically read 64k (or
365
 
        # whatever) chunks.
366
 
        try:
367
 
            my_sock, subproc_sock = socket.socketpair()
368
 
            osutils.set_fd_cloexec(my_sock)
369
 
        except (AttributeError, socket.error):
370
 
            # This platform doesn't support socketpair(), so just use ordinary
371
 
            # pipes instead.
372
 
            stdin = stdout = subprocess.PIPE
373
 
            my_sock, subproc_sock = None, None
374
 
        else:
375
 
            stdin = stdout = subproc_sock
376
 
        proc = subprocess.Popen(argv, stdin=stdin, stdout=stdout,
377
 
                                stderr=self._stderr_target,
378
 
                                bufsize=0,
 
366
        proc = subprocess.Popen(argv,
 
367
                                stdin=subprocess.PIPE,
 
368
                                stdout=subprocess.PIPE,
379
369
                                **os_specific_subprocess_params())
380
 
        if subproc_sock is not None:
381
 
            subproc_sock.close()
382
 
        return SSHSubprocessConnection(proc, sock=my_sock)
 
370
        return SSHSubprocess(proc)
383
371
 
384
372
    def connect_sftp(self, username, password, host, port):
385
373
        try:
387
375
                                                  subsystem='sftp')
388
376
            sock = self._connect(argv)
389
377
            return SFTPClient(SocketAsChannelAdapter(sock))
390
 
        except _ssh_connection_errors as e:
 
378
        except _sftp_connection_errors, e:
 
379
            self._raise_connection_error(host, port=port, orig_error=e)
 
380
        except (OSError, IOError), e:
 
381
            # If the machine is fast enough, ssh can actually exit
 
382
            # before we try and send it the sftp request, which
 
383
            # raises a Broken Pipe
 
384
            if e.errno not in (errno.EPIPE,):
 
385
                raise
391
386
            self._raise_connection_error(host, port=port, orig_error=e)
392
387
 
393
388
    def connect_ssh(self, username, password, host, port, command):
395
390
            argv = self._get_vendor_specific_argv(username, host, port,
396
391
                                                  command=command)
397
392
            return self._connect(argv)
398
 
        except _ssh_connection_errors as e:
 
393
        except (EOFError), e:
 
394
            self._raise_connection_error(host, port=port, orig_error=e)
 
395
        except (OSError, IOError), e:
 
396
            # If the machine is fast enough, ssh can actually exit
 
397
            # before we try and send it the sftp request, which
 
398
            # raises a Broken Pipe
 
399
            if e.errno not in (errno.EPIPE,):
 
400
                raise
399
401
            self._raise_connection_error(host, port=port, orig_error=e)
400
402
 
401
403
    def _get_vendor_specific_argv(self, username, host, port, subsystem=None,
416
418
                                  command=None):
417
419
        args = [self.executable_path,
418
420
                '-oForwardX11=no', '-oForwardAgent=no',
419
 
                '-oClearAllForwardings=yes',
 
421
                '-oClearAllForwardings=yes', '-oProtocol=2',
420
422
                '-oNoHostAuthenticationForLocalhost=yes']
421
423
        if port is not None:
422
424
            args.extend(['-p', str(port)])
423
425
        if username is not None:
424
426
            args.extend(['-l', username])
425
427
        if subsystem is not None:
426
 
            args.extend(['-s', '--', host, subsystem])
 
428
            args.extend(['-s', host, subsystem])
427
429
        else:
428
 
            args.extend(['--', host] + command)
 
430
            args.extend([host] + command)
429
431
        return args
430
432
 
431
 
 
432
433
register_ssh_vendor('openssh', OpenSSHSubprocessVendor())
433
434
 
434
435
 
439
440
 
440
441
    def _get_vendor_specific_argv(self, username, host, port, subsystem=None,
441
442
                                  command=None):
442
 
        self._check_hostname(host)
443
443
        args = [self.executable_path, '-x']
444
444
        if port is not None:
445
445
            args.extend(['-p', str(port)])
451
451
            args.extend([host] + command)
452
452
        return args
453
453
 
454
 
 
455
454
register_ssh_vendor('sshcorp', SSHCorpSubprocessVendor())
456
455
 
457
456
 
458
 
class LSHSubprocessVendor(SubprocessVendor):
459
 
    """SSH vendor that uses the 'lsh' executable from GNU"""
460
 
 
461
 
    executable_path = 'lsh'
462
 
 
463
 
    def _get_vendor_specific_argv(self, username, host, port, subsystem=None,
464
 
                                  command=None):
465
 
        self._check_hostname(host)
466
 
        args = [self.executable_path]
467
 
        if port is not None:
468
 
            args.extend(['-p', str(port)])
469
 
        if username is not None:
470
 
            args.extend(['-l', username])
471
 
        if subsystem is not None:
472
 
            args.extend(['--subsystem', subsystem, host])
473
 
        else:
474
 
            args.extend([host] + command)
475
 
        return args
476
 
 
477
 
 
478
 
register_ssh_vendor('lsh', LSHSubprocessVendor())
479
 
 
480
 
 
481
457
class PLinkSubprocessVendor(SubprocessVendor):
482
458
    """SSH vendor that uses the 'plink' executable from Putty."""
483
459
 
485
461
 
486
462
    def _get_vendor_specific_argv(self, username, host, port, subsystem=None,
487
463
                                  command=None):
488
 
        self._check_hostname(host)
489
464
        args = [self.executable_path, '-x', '-a', '-ssh', '-2', '-batch']
490
465
        if port is not None:
491
466
            args.extend(['-P', str(port)])
497
472
            args.extend([host] + command)
498
473
        return args
499
474
 
500
 
 
501
475
register_ssh_vendor('plink', PLinkSubprocessVendor())
502
476
 
503
477
 
508
482
    if username is None:
509
483
        username = auth.get_user('ssh', host, port=port,
510
484
                                 default=getpass.getuser())
511
 
    agent = paramiko.Agent()
512
 
    for key in agent.get_keys():
513
 
        trace.mutter('Trying SSH agent key %s'
514
 
                     % hexlify(key.get_fingerprint()).upper())
515
 
        try:
516
 
            paramiko_transport.auth_publickey(username, key)
517
 
            return
518
 
        except paramiko.SSHException as e:
519
 
            pass
 
485
    if _use_ssh_agent:
 
486
        agent = paramiko.Agent()
 
487
        for key in agent.get_keys():
 
488
            trace.mutter('Trying SSH agent key %s'
 
489
                         % paramiko.util.hexify(key.get_fingerprint()))
 
490
            try:
 
491
                paramiko_transport.auth_publickey(username, key)
 
492
                return
 
493
            except paramiko.SSHException, e:
 
494
                pass
520
495
 
521
496
    # okay, try finding id_rsa or id_dss?  (posix only)
522
497
    if _try_pkey_auth(paramiko_transport, paramiko.RSAKey, username, 'id_rsa'):
537
512
            paramiko_transport.auth_none(username)
538
513
        finally:
539
514
            paramiko_transport.logger.setLevel(old_level)
540
 
    except paramiko.BadAuthenticationType as e:
 
515
    except paramiko.BadAuthenticationType, e:
541
516
        # Supported methods are in the exception
542
517
        supported_auth_types = e.allowed_types
543
 
    except paramiko.SSHException as e:
 
518
    except paramiko.SSHException, e:
544
519
        # Don't know what happened, but just ignore it
545
520
        pass
546
521
    # We treat 'keyboard-interactive' and 'password' auth methods identically,
552
527
    # requires something other than a single password, but we currently don't
553
528
    # support that.
554
529
    if ('password' not in supported_auth_types and
555
 
            'keyboard-interactive' not in supported_auth_types):
 
530
        'keyboard-interactive' not in supported_auth_types):
556
531
        raise errors.ConnectionError('Unable to authenticate to SSH host as'
557
 
                                     '\n  %s@%s\nsupported auth types: %s'
558
 
                                     % (username, host, supported_auth_types))
 
532
            '\n  %s@%s\nsupported auth types: %s'
 
533
            % (username, host, supported_auth_types))
559
534
 
560
535
    if password:
561
536
        try:
562
537
            paramiko_transport.auth_password(username, password)
563
538
            return
564
 
        except paramiko.SSHException as e:
 
539
        except paramiko.SSHException, e:
565
540
            pass
566
541
 
567
542
    # give up and ask for a password
570
545
    if password is not None:
571
546
        try:
572
547
            paramiko_transport.auth_password(username, password)
573
 
        except paramiko.SSHException as e:
 
548
        except paramiko.SSHException, e:
574
549
            raise errors.ConnectionError(
575
550
                'Unable to authenticate to SSH host as'
576
551
                '\n  %s@%s\n' % (username, host), e)
587
562
        return True
588
563
    except paramiko.PasswordRequiredException:
589
564
        password = ui.ui_factory.get_password(
590
 
            prompt=u'SSH %(filename)s password',
591
 
            filename=filename.decode(osutils._fs_enc))
 
565
            prompt='SSH %(filename)s password', filename=filename)
592
566
        try:
593
567
            key = pkey_class.from_private_key_file(filename, password)
594
568
            paramiko_transport.auth_publickey(username, key)
604
578
    return False
605
579
 
606
580
 
607
 
def _ssh_host_keys_config_dir():
608
 
    return osutils.pathjoin(bedding.config_dir(), 'ssh_host_keys')
609
 
 
610
 
 
611
581
def load_host_keys():
612
582
    """
613
583
    Load system host keys (probably doesn't work on windows) and any
614
584
    "discovered" keys from previous sessions.
615
585
    """
616
 
    global SYSTEM_HOSTKEYS, BRZ_HOSTKEYS
 
586
    global SYSTEM_HOSTKEYS, BZR_HOSTKEYS
617
587
    try:
618
588
        SYSTEM_HOSTKEYS = paramiko.util.load_host_keys(
619
589
            os.path.expanduser('~/.ssh/known_hosts'))
620
 
    except IOError as e:
 
590
    except IOError, e:
621
591
        trace.mutter('failed to load system host keys: ' + str(e))
622
 
    brz_hostkey_path = _ssh_host_keys_config_dir()
 
592
    bzr_hostkey_path = osutils.pathjoin(config.config_dir(), 'ssh_host_keys')
623
593
    try:
624
 
        BRZ_HOSTKEYS = paramiko.util.load_host_keys(brz_hostkey_path)
625
 
    except IOError as e:
626
 
        trace.mutter('failed to load brz host keys: ' + str(e))
 
594
        BZR_HOSTKEYS = paramiko.util.load_host_keys(bzr_hostkey_path)
 
595
    except IOError, e:
 
596
        trace.mutter('failed to load bzr host keys: ' + str(e))
627
597
        save_host_keys()
628
598
 
629
599
 
631
601
    """
632
602
    Save "discovered" host keys in $(config)/ssh_host_keys/.
633
603
    """
634
 
    global SYSTEM_HOSTKEYS, BRZ_HOSTKEYS
635
 
    bzr_hostkey_path = _ssh_host_keys_config_dir()
636
 
    bedding.ensure_config_dir_exists()
 
604
    global SYSTEM_HOSTKEYS, BZR_HOSTKEYS
 
605
    bzr_hostkey_path = osutils.pathjoin(config.config_dir(), 'ssh_host_keys')
 
606
    config.ensure_config_dir_exists()
637
607
 
638
608
    try:
639
 
        with open(bzr_hostkey_path, 'w') as f:
640
 
            f.write('# SSH host keys collected by bzr\n')
641
 
            for hostname, keys in BRZ_HOSTKEYS.items():
642
 
                for keytype, key in keys.items():
643
 
                    f.write('%s %s %s\n' %
644
 
                            (hostname, keytype, key.get_base64()))
645
 
    except IOError as e:
 
609
        f = open(bzr_hostkey_path, 'w')
 
610
        f.write('# SSH host keys collected by bzr\n')
 
611
        for hostname, keys in BZR_HOSTKEYS.iteritems():
 
612
            for keytype, key in keys.iteritems():
 
613
                f.write('%s %s %s\n' % (hostname, keytype, key.get_base64()))
 
614
        f.close()
 
615
    except IOError, e:
646
616
        trace.mutter('failed to save bzr host keys: ' + str(e))
647
617
 
648
618
 
671
641
                'close_fds': True,
672
642
                }
673
643
 
674
 
 
675
644
import weakref
676
645
_subproc_weakrefs = set()
677
646
 
678
 
 
679
 
def _close_ssh_proc(proc, sock):
680
 
    """Carefully close stdin/stdout and reap the SSH process.
681
 
 
682
 
    If the pipes are already closed and/or the process has already been
683
 
    wait()ed on, that's ok, and no error is raised.  The goal is to do our best
684
 
    to clean up (whether or not a clean up was already tried).
685
 
    """
686
 
    funcs = []
687
 
    for closeable in (proc.stdin, proc.stdout, sock):
688
 
        # We expect that either proc (a subprocess.Popen) will have stdin and
689
 
        # stdout streams to close, or that we will have been passed a socket to
690
 
        # close, with the option not in use being None.
691
 
        if closeable is not None:
692
 
            funcs.append(closeable.close)
693
 
    funcs.append(proc.wait)
694
 
    for func in funcs:
 
647
def _close_ssh_proc(proc):
 
648
    for func in [proc.stdin.close, proc.stdout.close, proc.wait]:
695
649
        try:
696
650
            func()
697
651
        except OSError:
698
 
            # It's ok for the pipe to already be closed, or the process to
699
 
            # already be finished.
700
 
            continue
701
 
 
702
 
 
703
 
class SSHConnection(object):
704
 
    """Abstract base class for SSH connections."""
705
 
 
706
 
    def get_sock_or_pipes(self):
707
 
        """Returns a (kind, io_object) pair.
708
 
 
709
 
        If kind == 'socket', then io_object is a socket.
710
 
 
711
 
        If kind == 'pipes', then io_object is a pair of file-like objects
712
 
        (read_from, write_to).
713
 
        """
714
 
        raise NotImplementedError(self.get_sock_or_pipes)
715
 
 
716
 
    def close(self):
717
 
        raise NotImplementedError(self.close)
718
 
 
719
 
 
720
 
class SSHSubprocessConnection(SSHConnection):
721
 
    """A connection to an ssh subprocess via pipes or a socket.
722
 
 
723
 
    This class is also socket-like enough to be used with
724
 
    SocketAsChannelAdapter (it has 'send' and 'recv' methods).
725
 
    """
726
 
 
727
 
    def __init__(self, proc, sock=None):
728
 
        """Constructor.
729
 
 
730
 
        :param proc: a subprocess.Popen
731
 
        :param sock: if proc.stdin/out is a socket from a socketpair, then sock
732
 
            should breezy's half of that socketpair.  If not passed, proc's
733
 
            stdin/out is assumed to be ordinary pipes.
734
 
        """
 
652
            pass
 
653
 
 
654
 
 
655
class SSHSubprocess(object):
 
656
    """A socket-like object that talks to an ssh subprocess via pipes."""
 
657
 
 
658
    def __init__(self, proc):
735
659
        self.proc = proc
736
 
        self._sock = sock
737
660
        # Add a weakref to proc that will attempt to do the same as self.close
738
661
        # to avoid leaving processes lingering indefinitely.
739
 
 
740
662
        def terminate(ref):
741
663
            _subproc_weakrefs.remove(ref)
742
 
            _close_ssh_proc(proc, sock)
 
664
            _close_ssh_proc(proc)
743
665
        _subproc_weakrefs.add(weakref.ref(self, terminate))
744
666
 
745
667
    def send(self, data):
746
 
        if self._sock is not None:
747
 
            return self._sock.send(data)
748
 
        else:
749
 
            return os.write(self.proc.stdin.fileno(), data)
 
668
        return os.write(self.proc.stdin.fileno(), data)
750
669
 
751
670
    def recv(self, count):
752
 
        if self._sock is not None:
753
 
            return self._sock.recv(count)
754
 
        else:
755
 
            return os.read(self.proc.stdout.fileno(), count)
756
 
 
757
 
    def close(self):
758
 
        _close_ssh_proc(self.proc, self._sock)
759
 
 
760
 
    def get_sock_or_pipes(self):
761
 
        if self._sock is not None:
762
 
            return 'socket', self._sock
763
 
        else:
764
 
            return 'pipes', (self.proc.stdout, self.proc.stdin)
765
 
 
766
 
 
767
 
class _ParamikoSSHConnection(SSHConnection):
768
 
    """An SSH connection via paramiko."""
769
 
 
770
 
    def __init__(self, channel):
771
 
        self.channel = channel
772
 
 
773
 
    def get_sock_or_pipes(self):
774
 
        return ('socket', self.channel)
775
 
 
776
 
    def close(self):
777
 
        return self.channel.close()
 
671
        return os.read(self.proc.stdout.fileno(), count)
 
672
 
 
673
    def close(self):
 
674
        _close_ssh_proc(self.proc)
 
675
 
 
676
    def get_filelike_channels(self):
 
677
        return (self.proc.stdout, self.proc.stdin)
 
678