/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 breezy/transport/ssh.py

  • Committer: Jelmer Vernooij
  • Date: 2019-06-29 15:50:22 UTC
  • mto: This revision was merged to the branch mainline in revision 7374.
  • Revision ID: jelmer@jelmer.uk-20190629155022-jagfuax2bl39bjin
Drop file id roundtripping support in Git.

Stashing bzr-specific metadata in Git is fraught with problems. Instead, we'll
be opportunistic when trying to match files from bzr and git.

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