~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

Viewing changes to bzrlib/transport/sftp.py

[merge] John, sftp and others

Show diffs side-by-side

added added

removed removed

Lines of Context:
27
27
import time
28
28
import random
29
29
import subprocess
 
30
import weakref
30
31
 
31
32
from bzrlib.errors import (FileExists, 
32
33
                           TransportNotPossible, NoSuchFile, NonRelativePath,
35
36
from bzrlib.config import config_dir
36
37
from bzrlib.trace import mutter, warning, error
37
38
from bzrlib.transport import Transport, register_transport
 
39
from bzrlib.ui import ui_factory
38
40
 
39
41
try:
40
42
    import paramiko
52
54
if 'sftp' not in urlparse.uses_netloc: urlparse.uses_netloc.append('sftp')
53
55
 
54
56
 
 
57
_close_fds = True
 
58
if sys.platform == 'win32':
 
59
    # close_fds not supported on win32
 
60
    _close_fds = False
 
61
 
55
62
_ssh_vendor = None
56
63
def _get_ssh_vendor():
57
64
    """Find out what version of SSH is on the system."""
63
70
 
64
71
    try:
65
72
        p = subprocess.Popen(['ssh', '-V'],
66
 
                             close_fds=True,
 
73
                             close_fds=_close_fds,
67
74
                             stdin=subprocess.PIPE,
68
75
                             stdout=subprocess.PIPE,
69
76
                             stderr=subprocess.PIPE)
112
119
                args.extend(['-l', user])
113
120
            args.extend(['-s', 'sftp', hostname])
114
121
 
115
 
        self.proc = subprocess.Popen(args, close_fds=True,
 
122
        self.proc = subprocess.Popen(args, close_fds=_close_fds,
116
123
                                     stdin=subprocess.PIPE,
117
124
                                     stdout=subprocess.PIPE)
118
125
 
128
135
        self.proc.wait()
129
136
 
130
137
 
131
 
 
132
138
SYSTEM_HOSTKEYS = {}
133
139
BZR_HOSTKEYS = {}
134
140
 
 
141
# This is a weakref dictionary, so that we can reuse connections
 
142
# that are still active. Long term, it might be nice to have some
 
143
# sort of expiration policy, such as disconnect if inactive for
 
144
# X seconds. But that requires a lot more fanciness.
 
145
_connected_hosts = weakref.WeakValueDictionary()
 
146
 
135
147
def load_host_keys():
136
148
    """
137
149
    Load system host keys (probably doesn't work on windows) and any
208
220
    """
209
221
    Transport implementation for SFTP access.
210
222
    """
 
223
    _do_prefetch = False # Right now Paramiko's prefetch support causes things to hang
211
224
 
212
225
    def __init__(self, base, clone_from=None):
213
226
        assert base.startswith('sftp://')
 
227
        self._parse_url(base)
 
228
        base = self._unparse_url()
214
229
        super(SFTPTransport, self).__init__(base)
215
 
        self._parse_url(base)
216
230
        if clone_from is None:
217
231
            self._sftp_connect()
218
232
        else:
273
287
 
274
288
    def relpath(self, abspath):
275
289
        username, password, host, port, path = self._split_url(abspath)
276
 
        if (username != self._username or host != self._host or
277
 
            port != self._port or not path.startswith(self._path)):
278
 
            raise NonRelativePath('path %r is not under base URL %r'
279
 
                           % (abspath, self.base))
 
290
        error = []
 
291
        if (username != self._username):
 
292
            error.append('username mismatch')
 
293
        if (host != self._host):
 
294
            error.append('host mismatch')
 
295
        if (port != self._port):
 
296
            error.append('port mismatch')
 
297
        if (not path.startswith(self._path)):
 
298
            error.append('path mismatch')
 
299
        if error:
 
300
            raise NonRelativePath('path %r is not under base URL %r: %s'
 
301
                           % (abspath, self.base, ', '.join(error)))
280
302
        pl = len(self._path)
281
303
        return path[pl:].lstrip('/')
282
304
 
299
321
        try:
300
322
            path = self._abspath(relpath)
301
323
            f = self._sftp.file(path)
302
 
            try:
 
324
            if self._do_prefetch and hasattr(f, 'prefetch'):
303
325
                f.prefetch()
304
 
            except AttributeError:
305
 
                # only works on paramiko 1.5.1 or greater
306
 
                pass
307
326
            return f
308
327
        except (IOError, paramiko.SSHException), x:
309
328
            raise NoSuchFile('Error retrieving %s: %s' % (path, str(x)), x)
323
342
        # TODO: implement get_partial_multi to help with knit support
324
343
        f = self.get(relpath)
325
344
        f.seek(start)
326
 
        try:
 
345
        if self._do_prefetch and hasattr(f, 'prefetch'):
327
346
            f.prefetch()
328
 
        except AttributeError:
329
 
            # only works on paramiko 1.5.1 or greater
330
 
            pass
331
347
        return f
332
348
 
333
349
    def put(self, relpath, f):
369
385
                file_existed = True
370
386
            except:
371
387
                file_existed = False
 
388
            success = False
372
389
            try:
373
 
                self._sftp.rename(tmp_abspath, final_path)
374
 
            except IOError, e:
375
 
                self._translate_io_exception(e, relpath)
376
 
            except paramiko.SSHException, x:
377
 
                raise SFTPTransportError('Unable to rename into file %r' 
378
 
                                          % (relpath,), x)
379
 
            if file_existed:
380
 
                self._sftp.unlink(tmp_safety)
 
390
                try:
 
391
                    self._sftp.rename(tmp_abspath, final_path)
 
392
                except IOError, e:
 
393
                    self._translate_io_exception(e, relpath)
 
394
                except paramiko.SSHException, x:
 
395
                    raise SFTPTransportError('Unable to rename into file %r' % (path,), x) 
 
396
                else:
 
397
                    success = True
 
398
            finally:
 
399
                if file_existed:
 
400
                    if success:
 
401
                        self._sftp.unlink(tmp_safety)
 
402
                    else:
 
403
                        self._sftp.rename(tmp_safety, final_path)
381
404
 
382
405
    def iter_files_recursive(self):
383
406
        """Walk the relative paths of all files in this transport."""
430
453
        """Copy the item at rel_from to the location at rel_to"""
431
454
        path_from = self._abspath(rel_from)
432
455
        path_to = self._abspath(rel_to)
 
456
        self._copy_abspaths(path_from, path_to)
 
457
 
 
458
    def _copy_abspaths(self, path_from, path_to):
 
459
        """Copy files given an absolute path
 
460
 
 
461
        :param path_from: Path on remote server to read
 
462
        :param path_to: Path on remote server to write
 
463
        :return: None
 
464
 
 
465
        TODO: Should the destination location be atomically created?
 
466
              This has not been specified
 
467
        TODO: This should use some sort of remote copy, rather than
 
468
              pulling the data locally, and then writing it remotely
 
469
        """
433
470
        try:
434
471
            fin = self._sftp.file(path_from, 'rb')
435
472
            try:
444
481
        except (IOError, paramiko.SSHException), x:
445
482
            raise SFTPTransportError('Unable to copy %r to %r' % (path_from, path_to), x)
446
483
 
 
484
    def copy_to(self, relpaths, other, pb=None):
 
485
        """Copy a set of entries from self into another Transport.
 
486
 
 
487
        :param relpaths: A list/generator of entries to be copied.
 
488
        """
 
489
        if isinstance(other, SFTPTransport) and other._sftp is self._sftp:
 
490
            # Both from & to are on the same remote filesystem
 
491
            # We can use a remote copy, instead of pulling locally, and pushing
 
492
 
 
493
            total = self._get_total(relpaths)
 
494
            count = 0
 
495
            for path in relpaths:
 
496
                path_from = self._abspath(relpath)
 
497
                path_to = other._abspath(relpath)
 
498
                self._update_pb(pb, 'copy-to', count, total)
 
499
                self._copy_abspaths(path_from, path_to)
 
500
                count += 1
 
501
            return count
 
502
        else:
 
503
            return super(SFTPTransport, self).copy_to(relpaths, other, pb=pb)
 
504
 
 
505
        # The dummy implementation just does a simple get + put
 
506
        def copy_entry(path):
 
507
            other.put(path, self.get(path))
 
508
 
 
509
        return self._iterate_over(relpaths, copy_entry, pb, 'copy_to', expand=False)
 
510
 
447
511
    def move(self, rel_from, rel_to):
448
512
        """Move the item at rel_from to the location at rel_to"""
449
513
        path_from = self._abspath(rel_from)
567
631
         self._host, self._port, self._path) = self._split_url(url)
568
632
 
569
633
    def _sftp_connect(self):
 
634
        """Connect to the remote sftp server.
 
635
        After this, self._sftp should have a valid connection (or
 
636
        we raise an SFTPTransportError 'could not connect').
 
637
 
 
638
        TODO: Raise a more reasonable ConnectionFailed exception
 
639
        """
 
640
        global _connected_hosts
 
641
 
 
642
        idx = (self._host, self._port, self._username)
 
643
        try:
 
644
            self._sftp = _connected_hosts[idx]
 
645
            return
 
646
        except KeyError:
 
647
            pass
 
648
        
570
649
        vendor = _get_ssh_vendor()
571
 
        if (self._path is None) or (self._path == ''):
572
 
            self._path = ''
573
 
        else:
574
 
            # remove leading '/'
575
 
            self._path = urllib.unquote(self._path[1:])
576
650
        if vendor != 'none':
577
651
            sock = SFTPSubprocess(self._host, self._port, self._username)
578
652
            self._sftp = SFTPClient(sock)
579
653
        else:
580
654
            self._paramiko_connect()
581
655
 
 
656
        _connected_hosts[idx] = self._sftp
 
657
 
582
658
    def _paramiko_connect(self):
583
659
        global SYSTEM_HOSTKEYS, BZR_HOSTKEYS
584
660
        
585
661
        load_host_keys()
586
662
 
587
663
        try:
588
 
            t = paramiko.Transport((self._host, self._port or 22))
 
664
            t = paramiko.Transport((self._host, self._port))
589
665
            t.start_client()
590
666
        except paramiko.SSHException:
591
667
            raise SFTPTransportError('Unable to reach SSH host %s:%d' % (self._host, self._port))
614
690
                (self._host, our_server_key_hex, server_key_hex),
615
691
                ['Try editing %s or %s' % (filename1, filename2)])
616
692
 
617
 
        self._sftp_auth(t, self._username or getpass.getuser(), self._host)
 
693
        self._sftp_auth(t)
618
694
        
619
695
        try:
620
696
            self._sftp = t.open_sftp_client()
622
698
            raise BzrError('Unable to find path %s on SFTP server %s' % \
623
699
                (self._path, self._host))
624
700
 
625
 
    def _sftp_auth(self, transport, username, host):
 
701
    def _sftp_auth(self, transport):
 
702
        # paramiko requires a username, but it might be none if nothing was supplied
 
703
        # use the local username, just in case.
 
704
        # We don't override self._username, because if we aren't using paramiko,
 
705
        # the username might be specified in ~/.ssh/config and we don't want to
 
706
        # force it to something else
 
707
        # Also, it would mess up the self.relpath() functionality
 
708
        username = self._username or getpass.getuser()
 
709
 
626
710
        agent = paramiko.Agent()
627
711
        for key in agent.get_keys():
628
712
            mutter('Trying SSH agent key %s' % paramiko.util.hexify(key.get_fingerprint()))
629
713
            try:
630
 
                transport.auth_publickey(self._username, key)
 
714
                transport.auth_publickey(username, key)
631
715
                return
632
716
            except paramiko.SSHException, e:
633
717
                pass
634
718
        
635
719
        # okay, try finding id_rsa or id_dss?  (posix only)
636
 
        if self._try_pkey_auth(transport, paramiko.RSAKey, 'id_rsa'):
637
 
            return
638
 
        if self._try_pkey_auth(transport, paramiko.DSSKey, 'id_dsa'):
639
 
            return
 
720
        if self._try_pkey_auth(transport, paramiko.RSAKey, username, 'id_rsa'):
 
721
            return
 
722
        if self._try_pkey_auth(transport, paramiko.DSSKey, username, 'id_dsa'):
 
723
            return
 
724
 
640
725
 
641
726
        if self._password:
642
727
            try:
643
 
                transport.auth_password(self._username, self._password)
 
728
                transport.auth_password(username, self._password)
644
729
                return
645
730
            except paramiko.SSHException, e:
646
731
                pass
647
732
 
 
733
            # FIXME: Don't keep a password held in memory if you can help it
 
734
            #self._password = None
 
735
 
648
736
        # give up and ask for a password
649
 
        # FIXME: shouldn't be implementing UI this deep into bzrlib
650
 
        enc = sys.stdout.encoding
651
 
        password = getpass.getpass('SSH %s@%s password: ' %
652
 
            (self._username.encode(enc, 'replace'), self._host.encode(enc, 'replace')))
 
737
        password = ui_factory.get_password(prompt='SSH %(user)s@%(host)s password',
 
738
                                           user=username, host=self._host)
653
739
        try:
654
 
            transport.auth_password(self._username, password)
 
740
            transport.auth_password(username, password)
655
741
        except paramiko.SSHException:
656
742
            raise SFTPTransportError('Unable to authenticate to SSH host as %s@%s' % \
657
 
                (self._username, self._host))
 
743
                (username, self._host))
658
744
 
659
 
    def _try_pkey_auth(self, transport, pkey_class, filename):
 
745
    def _try_pkey_auth(self, transport, pkey_class, username, filename):
660
746
        filename = os.path.expanduser('~/.ssh/' + filename)
661
747
        try:
662
748
            key = pkey_class.from_private_key_file(filename)
663
 
            transport.auth_publickey(self._username, key)
 
749
            transport.auth_publickey(username, key)
664
750
            return True
665
751
        except paramiko.PasswordRequiredException:
666
 
            # FIXME: shouldn't be implementing UI this deep into bzrlib
667
 
            enc = sys.stdout.encoding
668
 
            password = getpass.getpass('SSH %s password: ' % 
669
 
                (os.path.basename(filename).encode(enc, 'replace'),))
 
752
            password = ui_factory.get_password(prompt='SSH %(filename)s password',
 
753
                                               filename=filename)
670
754
            try:
671
755
                key = pkey_class.from_private_key_file(filename, password)
672
 
                transport.auth_publickey(self._username, key)
 
756
                transport.auth_publickey(username, key)
673
757
                return True
674
758
            except paramiko.SSHException:
675
759
                mutter('SSH authentication via %s key failed.' % (os.path.basename(filename),))
692
776
 
693
777
        :param relpath: The relative path, where the file should be opened
694
778
        """
695
 
        path = self._abspath(relpath)
 
779
        path = self._sftp._adjust_cwd(self._abspath(relpath))
696
780
        attr = SFTPAttributes()
697
781
        mode = (SFTP_FLAG_WRITE | SFTP_FLAG_CREATE 
698
782
                | SFTP_FLAG_TRUNC | SFTP_FLAG_EXCL)