~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

Viewing changes to bzrlib/transport/sftp.py

[merge] john

Show diffs side-by-side

added added

removed removed

Lines of Context:
30
30
import weakref
31
31
 
32
32
from bzrlib.errors import (FileExists, 
33
 
                           TransportNotPossible, NoSuchFile, NonRelativePath,
 
33
                           TransportNotPossible, NoSuchFile, PathNotChild,
34
34
                           TransportError,
35
35
                           LockError)
36
36
from bzrlib.config import config_dir
37
37
from bzrlib.trace import mutter, warning, error
38
38
from bzrlib.transport import Transport, register_transport
39
 
from bzrlib.ui import ui_factory
 
39
import bzrlib.ui
40
40
 
41
41
try:
42
42
    import paramiko
180
180
        mutter('failed to save bzr host keys: ' + str(e))
181
181
 
182
182
 
183
 
 
184
 
class SFTPTransportError (TransportError):
185
 
    pass
186
 
 
187
183
class SFTPLock(object):
188
184
    """This fakes a lock in a remote location."""
189
185
    __slots__ = ['path', 'lock_path', 'lock_file', 'transport']
297
293
        if (not path.startswith(self._path)):
298
294
            error.append('path mismatch')
299
295
        if error:
300
 
            raise NonRelativePath('path %r is not under base URL %r: %s'
301
 
                           % (abspath, self.base, ', '.join(error)))
 
296
            extra = ': ' + ', '.join(error)
 
297
            raise PathNotChild(abspath, self.base, extra=extra)
302
298
        pl = len(self._path)
303
299
        return path[pl:].lstrip('/')
304
300
 
324
320
            if self._do_prefetch and hasattr(f, 'prefetch'):
325
321
                f.prefetch()
326
322
            return f
327
 
        except (IOError, paramiko.SSHException), x:
328
 
            raise NoSuchFile('Error retrieving %s: %s' % (path, str(x)), x)
 
323
        except (IOError, paramiko.SSHException), e:
 
324
            self._translate_io_exception(e, path, ': error retrieving')
329
325
 
330
326
    def get_partial(self, relpath, start, length=None):
331
327
        """
362
358
        try:
363
359
            try:
364
360
                self._pump(f, fout)
365
 
            except IOError, e:
366
 
                self._translate_io_exception(e, relpath)
367
 
            except paramiko.SSHException, x:
368
 
                raise SFTPTransportError('Unable to write file %r' % (relpath,), x)
 
361
            except (paramiko.SSHException, IOError), e:
 
362
                self._translate_io_exception(e, relpath, ': unable to write')
369
363
        except Exception, e:
370
364
            # If we fail, try to clean up the temporary file
371
365
            # before we throw the exception
389
383
            try:
390
384
                try:
391
385
                    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) 
 
386
                except (paramiko.SSHException, IOError), e:
 
387
                    self._translate_io_exception(e, relpath, ': unable to rename')
396
388
                else:
397
389
                    success = True
398
390
            finally:
419
411
        try:
420
412
            path = self._abspath(relpath)
421
413
            self._sftp.mkdir(path)
422
 
        except IOError, e:
423
 
            self._translate_io_exception(e, relpath)
424
 
        except (IOError, paramiko.SSHException), x:
425
 
            raise SFTPTransportError('Unable to mkdir %r' % (path,), x)
426
 
 
427
 
    def _translate_io_exception(self, e, relpath):
 
414
        except (paramiko.SSHException, IOError), e:
 
415
            self._translate_io_exception(e, relpath, ': unable to mkdir',
 
416
                failure_exc=FileExists)
 
417
 
 
418
    def _translate_io_exception(self, e, path, more_info='', failure_exc=NoSuchFile):
 
419
        """Translate a paramiko or IOError into a friendlier exception.
 
420
 
 
421
        :param e: The original exception
 
422
        :param path: The path in question when the error is raised
 
423
        :param more_info: Extra information that can be included,
 
424
                          such as what was going on
 
425
        :param failure_exc: Paramiko has the super fun ability to raise completely
 
426
                           opaque errors that just set "e.args = ('Failure',)" with
 
427
                           no more information.
 
428
                           This sometimes means FileExists, but it also sometimes
 
429
                           means NoSuchFile
 
430
        """
428
431
        # paramiko seems to generate detailless errors.
429
 
        if (e.errno == errno.ENOENT or
430
 
            e.args == ('No such file or directory',) or
431
 
            e.args == ('No such file',)):
432
 
            raise NoSuchFile(relpath)
433
 
        if (e.args == ('mkdir failed',)):
434
 
            raise FileExists(relpath)
435
 
        # strange but true, for the paramiko server.
436
 
        if (e.args == ('Failure',)):
437
 
            raise FileExists(relpath)
438
 
        raise
 
432
        self._translate_error(e, path, raise_generic=False)
 
433
        if hasattr(e, 'args'):
 
434
            if (e.args == ('No such file or directory',) or
 
435
                e.args == ('No such file',)):
 
436
                raise NoSuchFile(path, str(e) + more_info)
 
437
            if (e.args == ('mkdir failed',)):
 
438
                raise FileExists(path, str(e) + more_info)
 
439
            # strange but true, for the paramiko server.
 
440
            if (e.args == ('Failure',)):
 
441
                raise failure_exc(path, str(e) + more_info)
 
442
        raise e
439
443
 
440
444
    def append(self, relpath, f):
441
445
        """
446
450
            path = self._abspath(relpath)
447
451
            fout = self._sftp.file(path, 'ab')
448
452
            self._pump(f, fout)
449
 
        except (IOError, paramiko.SSHException), x:
450
 
            raise SFTPTransportError('Unable to append file %r' % (path,), x)
 
453
        except (IOError, paramiko.SSHException), e:
 
454
            self._translate_io_exception(e, relpath, ': unable to append')
451
455
 
452
456
    def copy(self, rel_from, rel_to):
453
457
        """Copy the item at rel_from to the location at rel_to"""
478
482
                    fout.close()
479
483
            finally:
480
484
                fin.close()
481
 
        except (IOError, paramiko.SSHException), x:
482
 
            raise SFTPTransportError('Unable to copy %r to %r' % (path_from, path_to), x)
 
485
        except (IOError, paramiko.SSHException), e:
 
486
            self._translate_io_exception(e, path_from, ': unable copy to: %r' % path_to)
483
487
 
484
488
    def copy_to(self, relpaths, other, pb=None):
485
489
        """Copy a set of entries from self into another Transport.
514
518
        path_to = self._abspath(rel_to)
515
519
        try:
516
520
            self._sftp.rename(path_from, path_to)
517
 
        except (IOError, paramiko.SSHException), x:
518
 
            raise SFTPTransportError('Unable to move %r to %r' % (path_from, path_to), x)
 
521
        except (IOError, paramiko.SSHException), e:
 
522
            self._translate_io_exception(e, path_from, ': unable to move to: %r' % path_to)
519
523
 
520
524
    def delete(self, relpath):
521
525
        """Delete the item at relpath"""
522
526
        path = self._abspath(relpath)
523
527
        try:
524
528
            self._sftp.remove(path)
525
 
        except (IOError, paramiko.SSHException), x:
526
 
            raise SFTPTransportError('Unable to delete %r' % (path,), x)
 
529
        except (IOError, paramiko.SSHException), e:
 
530
            self._translate_io_exception(e, path, ': unable to delete')
527
531
            
528
532
    def listable(self):
529
533
        """Return True if this store supports listing."""
537
541
        path = self._abspath(relpath)
538
542
        try:
539
543
            return self._sftp.listdir(path)
540
 
        except (IOError, paramiko.SSHException), x:
541
 
            raise SFTPTransportError('Unable to list folder %r' % (path,), x)
 
544
        except (IOError, paramiko.SSHException), e:
 
545
            self._translate_io_exception(e, path, ': failed to list_dir')
542
546
 
543
547
    def stat(self, relpath):
544
548
        """Return the stat information for a file."""
545
549
        path = self._abspath(relpath)
546
550
        try:
547
551
            return self._sftp.stat(path)
548
 
        except (IOError, paramiko.SSHException), x:
549
 
            raise SFTPTransportError('Unable to stat %r' % (path,), x)
 
552
        except (IOError, paramiko.SSHException), e:
 
553
            self._translate_io_exception(e, path, ': unable to stat')
550
554
 
551
555
    def lock_read(self, relpath):
552
556
        """
612
616
            try:
613
617
                port = int(port)
614
618
            except ValueError:
615
 
                raise SFTPTransportError('%s: invalid port number' % port)
 
619
                # TODO: Should this be ConnectionError?
 
620
                raise TransportError('%s: invalid port number' % port)
616
621
        host = urllib.unquote(host)
617
622
 
618
623
        path = urllib.unquote(path)
633
638
    def _sftp_connect(self):
634
639
        """Connect to the remote sftp server.
635
640
        After this, self._sftp should have a valid connection (or
636
 
        we raise an SFTPTransportError 'could not connect').
 
641
        we raise an TransportError 'could not connect').
637
642
 
638
643
        TODO: Raise a more reasonable ConnectionFailed exception
639
644
        """
661
666
        load_host_keys()
662
667
 
663
668
        try:
664
 
            t = paramiko.Transport((self._host, self._port))
 
669
            t = paramiko.Transport((self._host, self._port or 22))
665
670
            t.start_client()
666
 
        except paramiko.SSHException:
667
 
            raise SFTPTransportError('Unable to reach SSH host %s:%d' % (self._host, self._port))
 
671
        except paramiko.SSHException, e:
 
672
            raise ConnectionError('Unable to reach SSH host %s:%d' %
 
673
                                  (self._host, self._port), e)
668
674
            
669
675
        server_key = t.get_remote_server_key()
670
676
        server_key_hex = paramiko.util.hexify(server_key.get_fingerprint())
686
692
        if server_key != our_server_key:
687
693
            filename1 = os.path.expanduser('~/.ssh/known_hosts')
688
694
            filename2 = os.path.join(config_dir(), 'ssh_host_keys')
689
 
            raise SFTPTransportError('Host keys for %s do not match!  %s != %s' % \
 
695
            raise TransportError('Host keys for %s do not match!  %s != %s' % \
690
696
                (self._host, our_server_key_hex, server_key_hex),
691
697
                ['Try editing %s or %s' % (filename1, filename2)])
692
698
 
694
700
        
695
701
        try:
696
702
            self._sftp = t.open_sftp_client()
697
 
        except paramiko.SSHException:
698
 
            raise BzrError('Unable to find path %s on SFTP server %s' % \
699
 
                (self._path, self._host))
 
703
        except paramiko.SSHException, e:
 
704
            raise ConnectionError('Unable to start sftp client %s:%d' %
 
705
                                  (self._host, self._port), e)
700
706
 
701
707
    def _sftp_auth(self, transport):
702
708
        # paramiko requires a username, but it might be none if nothing was supplied
707
713
        # Also, it would mess up the self.relpath() functionality
708
714
        username = self._username or getpass.getuser()
709
715
 
710
 
        agent = paramiko.Agent()
711
 
        for key in agent.get_keys():
712
 
            mutter('Trying SSH agent key %s' % paramiko.util.hexify(key.get_fingerprint()))
713
 
            try:
714
 
                transport.auth_publickey(username, key)
715
 
                return
716
 
            except paramiko.SSHException, e:
717
 
                pass
 
716
        # Paramiko tries to open a socket.AF_UNIX in order to connect
 
717
        # to ssh-agent. That attribute doesn't exist on win32 (it does in cygwin)
 
718
        # so we get an AttributeError exception. For now, just don't try to
 
719
        # connect to an agent if we are on win32
 
720
        if sys.platform != 'win32':
 
721
            agent = paramiko.Agent()
 
722
            for key in agent.get_keys():
 
723
                mutter('Trying SSH agent key %s' % paramiko.util.hexify(key.get_fingerprint()))
 
724
                try:
 
725
                    transport.auth_publickey(username, key)
 
726
                    return
 
727
                except paramiko.SSHException, e:
 
728
                    pass
718
729
        
719
730
        # okay, try finding id_rsa or id_dss?  (posix only)
720
731
        if self._try_pkey_auth(transport, paramiko.RSAKey, username, 'id_rsa'):
734
745
            #self._password = None
735
746
 
736
747
        # give up and ask for a password
737
 
        password = ui_factory.get_password(prompt='SSH %(user)s@%(host)s password',
738
 
                                           user=username, host=self._host)
 
748
        password = bzrlib.ui.ui_factory.get_password(
 
749
                prompt='SSH %(user)s@%(host)s password',
 
750
                user=username, host=self._host)
739
751
        try:
740
752
            transport.auth_password(username, password)
741
 
        except paramiko.SSHException:
742
 
            raise SFTPTransportError('Unable to authenticate to SSH host as %s@%s' % \
743
 
                (username, self._host))
 
753
        except paramiko.SSHException, e:
 
754
            raise ConnectionError('Unable to authenticate to SSH host as %s@%s' %
 
755
                                  (username, self._host), e)
744
756
 
745
757
    def _try_pkey_auth(self, transport, pkey_class, username, filename):
746
758
        filename = os.path.expanduser('~/.ssh/' + filename)
749
761
            transport.auth_publickey(username, key)
750
762
            return True
751
763
        except paramiko.PasswordRequiredException:
752
 
            password = ui_factory.get_password(prompt='SSH %(filename)s password',
753
 
                                               filename=filename)
 
764
            password = bzrlib.ui.ui_factory.get_password(
 
765
                    prompt='SSH %(filename)s password',
 
766
                    filename=filename)
754
767
            try:
755
768
                key = pkey_class.from_private_key_file(filename, password)
756
769
                transport.auth_publickey(username, key)
783
796
        try:
784
797
            t, msg = self._sftp._request(CMD_OPEN, path, mode, attr)
785
798
            if t != CMD_HANDLE:
786
 
                raise SFTPTransportError('Expected an SFTP handle')
 
799
                raise TransportError('Expected an SFTP handle')
787
800
            handle = msg.get_string()
788
801
            return SFTPFile(self._sftp, handle, 'w', -1)
789
 
        except IOError, e:
790
 
            self._translate_io_exception(e, relpath)
791
 
        except paramiko.SSHException, x:
792
 
            raise SFTPTransportError('Unable to open file %r' % (path,), x)
 
802
        except (paramiko.SSHException, IOError), e:
 
803
            self._translate_io_exception(e, relpath, ': unable to open',
 
804
                failure_exc=FileExists)
793
805