~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

Viewing changes to bzrlib/transport/sftp.py

[merge] jam-integration

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
128
130
        self.proc.wait()
129
131
 
130
132
 
131
 
 
132
133
SYSTEM_HOSTKEYS = {}
133
134
BZR_HOSTKEYS = {}
134
135
 
 
136
# This is a weakref dictionary, so that we can reuse connections
 
137
# that are still active. Long term, it might be nice to have some
 
138
# sort of expiration policy, such as disconnect if inactive for
 
139
# X seconds. But that requires a lot more fanciness.
 
140
_connected_hosts = weakref.WeakValueDictionary()
 
141
 
135
142
def load_host_keys():
136
143
    """
137
144
    Load system host keys (probably doesn't work on windows) and any
208
215
    """
209
216
    Transport implementation for SFTP access.
210
217
    """
 
218
    _do_prefetch = False # Right now Paramiko's prefetch support causes things to hang
211
219
 
212
220
    def __init__(self, base, clone_from=None):
213
221
        assert base.startswith('sftp://')
 
222
        self._parse_url(base)
 
223
        base = self._unparse_url()
214
224
        super(SFTPTransport, self).__init__(base)
215
 
        self._parse_url(base)
216
225
        if clone_from is None:
217
226
            self._sftp_connect()
218
227
        else:
299
308
        try:
300
309
            path = self._abspath(relpath)
301
310
            f = self._sftp.file(path)
302
 
            try:
 
311
            if self._do_prefetch and hasattr(f, 'prefetch'):
303
312
                f.prefetch()
304
 
            except AttributeError:
305
 
                # only works on paramiko 1.5.1 or greater
306
 
                pass
307
313
            return f
308
314
        except (IOError, paramiko.SSHException), x:
309
315
            raise NoSuchFile('Error retrieving %s: %s' % (path, str(x)), x)
323
329
        # TODO: implement get_partial_multi to help with knit support
324
330
        f = self.get(relpath)
325
331
        f.seek(start)
326
 
        try:
 
332
        if self._do_prefetch and hasattr(f, 'prefetch'):
327
333
            f.prefetch()
328
 
        except AttributeError:
329
 
            # only works on paramiko 1.5.1 or greater
330
 
            pass
331
334
        return f
332
335
 
333
336
    def put(self, relpath, f):
369
372
                file_existed = True
370
373
            except:
371
374
                file_existed = False
 
375
            success = False
372
376
            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)
 
377
                try:
 
378
                    self._sftp.rename(tmp_abspath, final_path)
 
379
                except IOError, e:
 
380
                    self._translate_io_exception(e, relpath)
 
381
                except paramiko.SSHException, x:
 
382
                    raise SFTPTransportError('Unable to rename into file %r' % (path,), x) 
 
383
                else:
 
384
                    success = True
 
385
            finally:
 
386
                if file_existed:
 
387
                    if success:
 
388
                        self._sftp.unlink(tmp_safety)
 
389
                    else:
 
390
                        self._sftp.rename(tmp_safety, final_path)
381
391
 
382
392
    def iter_files_recursive(self):
383
393
        """Walk the relative paths of all files in this transport."""
430
440
        """Copy the item at rel_from to the location at rel_to"""
431
441
        path_from = self._abspath(rel_from)
432
442
        path_to = self._abspath(rel_to)
 
443
        self._copy_abspaths(path_from, path_to)
 
444
 
 
445
    def _copy_abspaths(self, path_from, path_to):
 
446
        """Copy files given an absolute path
 
447
 
 
448
        :param path_from: Path on remote server to read
 
449
        :param path_to: Path on remote server to write
 
450
        :return: None
 
451
 
 
452
        TODO: Should the destination location be atomically created?
 
453
              This has not been specified
 
454
        TODO: This should use some sort of remote copy, rather than
 
455
              pulling the data locally, and then writing it remotely
 
456
        """
433
457
        try:
434
458
            fin = self._sftp.file(path_from, 'rb')
435
459
            try:
444
468
        except (IOError, paramiko.SSHException), x:
445
469
            raise SFTPTransportError('Unable to copy %r to %r' % (path_from, path_to), x)
446
470
 
 
471
    def copy_to(self, relpaths, other, pb=None):
 
472
        """Copy a set of entries from self into another Transport.
 
473
 
 
474
        :param relpaths: A list/generator of entries to be copied.
 
475
        """
 
476
        if isinstance(other, SFTPTransport) and other._sftp is self._sftp:
 
477
            # Both from & to are on the same remote filesystem
 
478
            # We can use a remote copy, instead of pulling locally, and pushing
 
479
 
 
480
            total = self._get_total(relpaths)
 
481
            count = 0
 
482
            for path in relpaths:
 
483
                path_from = self._abspath(relpath)
 
484
                path_to = other._abspath(relpath)
 
485
                self._update_pb(pb, 'copy-to', count, total)
 
486
                self._copy_abspaths(path_from, path_to)
 
487
                count += 1
 
488
            return count
 
489
        else:
 
490
            return super(SFTPTransport, self).copy_to(relpaths, other, pb=pb)
 
491
 
 
492
        # The dummy implementation just does a simple get + put
 
493
        def copy_entry(path):
 
494
            other.put(path, self.get(path))
 
495
 
 
496
        return self._iterate_over(relpaths, copy_entry, pb, 'copy_to', expand=False)
 
497
 
447
498
    def move(self, rel_from, rel_to):
448
499
        """Move the item at rel_from to the location at rel_to"""
449
500
        path_from = self._abspath(rel_from)
522
573
        netloc = urllib.quote(self._host)
523
574
        if self._username is not None:
524
575
            netloc = '%s@%s' % (urllib.quote(self._username), netloc)
525
 
        if self._port is not None:
 
576
        if self._port not in (None, 22):
526
577
            netloc = '%s:%d' % (netloc, self._port)
527
578
 
528
579
        return urlparse.urlunparse(('sftp', netloc, path, '', '', ''))
565
616
    def _parse_url(self, url):
566
617
        (self._username, self._password,
567
618
         self._host, self._port, self._path) = self._split_url(url)
 
619
        if self._port is None:
 
620
            self._port = 22
568
621
 
569
622
    def _sftp_connect(self):
 
623
        """Connect to the remote sftp server.
 
624
        After this, self._sftp should have a valid connection (or
 
625
        we raise an SFTPTransportError 'could not connect').
 
626
 
 
627
        TODO: Raise a more reasonable ConnectionFailed exception
 
628
        """
 
629
        global _connected_hosts
 
630
 
 
631
        idx = (self._host, self._port, self._username)
 
632
        try:
 
633
            self._sftp = _connected_hosts[idx]
 
634
            return
 
635
        except KeyError:
 
636
            pass
 
637
        
570
638
        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
639
        if vendor != 'none':
577
640
            sock = SFTPSubprocess(self._host, self._port, self._username)
578
641
            self._sftp = SFTPClient(sock)
579
642
        else:
580
643
            self._paramiko_connect()
581
644
 
 
645
        _connected_hosts[idx] = self._sftp
 
646
 
582
647
    def _paramiko_connect(self):
583
648
        global SYSTEM_HOSTKEYS, BZR_HOSTKEYS
584
649
        
585
650
        load_host_keys()
586
651
 
587
652
        try:
588
 
            t = paramiko.Transport((self._host, self._port or 22))
 
653
            t = paramiko.Transport((self._host, self._port))
589
654
            t.start_client()
590
655
        except paramiko.SSHException:
591
656
            raise SFTPTransportError('Unable to reach SSH host %s:%d' % (self._host, self._port))
614
679
                (self._host, our_server_key_hex, server_key_hex),
615
680
                ['Try editing %s or %s' % (filename1, filename2)])
616
681
 
617
 
        self._sftp_auth(t, self._username or getpass.getuser(), self._host)
 
682
        self._sftp_auth(t)
618
683
        
619
684
        try:
620
685
            self._sftp = t.open_sftp_client()
622
687
            raise BzrError('Unable to find path %s on SFTP server %s' % \
623
688
                (self._path, self._host))
624
689
 
625
 
    def _sftp_auth(self, transport, username, host):
 
690
    def _sftp_auth(self, transport):
 
691
        # paramiko requires a username, but it might be none if nothing was supplied
 
692
        # use the local username, just in case.
 
693
        # We don't override self._username, because if we aren't using paramiko,
 
694
        # the username might be specified in ~/.ssh/config and we don't want to
 
695
        # force it to something else
 
696
        # Also, it would mess up the self.relpath() functionality
 
697
        username = self._username or getpass.getuser()
 
698
 
626
699
        agent = paramiko.Agent()
627
700
        for key in agent.get_keys():
628
701
            mutter('Trying SSH agent key %s' % paramiko.util.hexify(key.get_fingerprint()))
629
702
            try:
630
 
                transport.auth_publickey(self._username, key)
 
703
                transport.auth_publickey(username, key)
631
704
                return
632
705
            except paramiko.SSHException, e:
633
706
                pass
634
707
        
635
708
        # 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
 
709
        if self._try_pkey_auth(transport, paramiko.RSAKey, username, 'id_rsa'):
 
710
            return
 
711
        if self._try_pkey_auth(transport, paramiko.DSSKey, username, 'id_dsa'):
 
712
            return
 
713
 
640
714
 
641
715
        if self._password:
642
716
            try:
643
 
                transport.auth_password(self._username, self._password)
 
717
                transport.auth_password(username, self._password)
644
718
                return
645
719
            except paramiko.SSHException, e:
646
720
                pass
647
721
 
 
722
            # FIXME: Don't keep a password held in memory if you can help it
 
723
            #self._password = None
 
724
 
648
725
        # 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')))
 
726
        password = ui_factory.get_password(prompt='SSH %(user)s@%(host)s password',
 
727
                                           user=username, host=self._host)
653
728
        try:
654
 
            transport.auth_password(self._username, password)
 
729
            transport.auth_password(username, password)
655
730
        except paramiko.SSHException:
656
731
            raise SFTPTransportError('Unable to authenticate to SSH host as %s@%s' % \
657
 
                (self._username, self._host))
 
732
                (username, self._host))
658
733
 
659
 
    def _try_pkey_auth(self, transport, pkey_class, filename):
 
734
    def _try_pkey_auth(self, transport, pkey_class, username, filename):
660
735
        filename = os.path.expanduser('~/.ssh/' + filename)
661
736
        try:
662
737
            key = pkey_class.from_private_key_file(filename)
663
 
            transport.auth_publickey(self._username, key)
 
738
            transport.auth_publickey(username, key)
664
739
            return True
665
740
        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'),))
 
741
            password = ui_factory.get_password(prompt='SSH %(filename)s password',
 
742
                                               filename=filename)
670
743
            try:
671
744
                key = pkey_class.from_private_key_file(filename, password)
672
 
                transport.auth_publickey(self._username, key)
 
745
                transport.auth_publickey(username, key)
673
746
                return True
674
747
            except paramiko.SSHException:
675
748
                mutter('SSH authentication via %s key failed.' % (os.path.basename(filename),))
692
765
 
693
766
        :param relpath: The relative path, where the file should be opened
694
767
        """
695
 
        path = self._abspath(relpath)
 
768
        path = self._sftp._adjust_cwd(self._abspath(relpath))
696
769
        attr = SFTPAttributes()
697
770
        mode = (SFTP_FLAG_WRITE | SFTP_FLAG_CREATE 
698
771
                | SFTP_FLAG_TRUNC | SFTP_FLAG_EXCL)