~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

Viewing changes to bzrlib/transport/sftp.py

 * Test sftp with relative, absolute-in-homedir and absolute-not-in-homedir
   paths for the transport tests. Introduce blackbox remote sftp tests that
   test the same permutations. (Robert Collins, Robey Pointer)

 * Transport implementation tests are now independent of the local file
   system, which allows tests for esoteric transports, and for features
   not available in the local file system. They also repeat for variations
   on the URL scheme that can introduce issues in the transport code,
   see bzrlib.transport.TransportTestProviderAdapter() for this.
   (Robert Collins).

 * TestCase.build_tree uses the transport interface to build trees, pass
   in a transport parameter to give it an existing connection.
   (Robert Collins).

Show diffs side-by-side

added added

removed removed

Lines of Context:
29
29
import subprocess
30
30
import weakref
31
31
 
32
 
from bzrlib.errors import (FileExists, 
 
32
from bzrlib.config import config_dir, ensure_config_dir_exists
 
33
from bzrlib.errors import (ConnectionError,
 
34
                           FileExists, 
33
35
                           TransportNotPossible, NoSuchFile, PathNotChild,
34
36
                           TransportError,
35
 
                           LockError)
36
 
from bzrlib.config import config_dir, ensure_config_dir_exists
 
37
                           LockError
 
38
                           )
 
39
from bzrlib.osutils import pathjoin, fancy_rename
37
40
from bzrlib.trace import mutter, warning, error
38
 
from bzrlib.transport import Transport, register_transport
39
 
from bzrlib.osutils import pathjoin, fancy_rename
 
41
from bzrlib.transport import Transport, Server, urlescape
40
42
import bzrlib.ui
41
43
 
42
44
try:
99
101
 
100
102
class SFTPSubprocess:
101
103
    """A socket-like object that talks to an ssh subprocess via pipes."""
102
 
    def __init__(self, hostname, port=None, user=None):
103
 
        vendor = _get_ssh_vendor()
 
104
    def __init__(self, hostname, vendor, port=None, user=None):
104
105
        assert vendor in ['openssh', 'ssh']
105
106
        if vendor == 'openssh':
106
107
            args = ['ssh',
199
200
        self.lock_path = path + '.write-lock'
200
201
        self.transport = transport
201
202
        try:
202
 
            abspath = transport._abspath(self.lock_path)
 
203
            # RBC 20060103 FIXME should we be using private methods here ?
 
204
            abspath = transport._remote_path(self.lock_path)
203
205
            self.lock_file = transport._sftp_open_exclusive(abspath)
204
206
        except FileExists:
205
207
            raise LockError('File %r already locked' % (self.path,))
221
223
            # What specific errors should we catch here?
222
224
            pass
223
225
 
 
226
 
 
227
 
224
228
class SFTPTransport (Transport):
225
229
    """
226
230
    Transport implementation for SFTP access.
231
235
        assert base.startswith('sftp://')
232
236
        self._parse_url(base)
233
237
        base = self._unparse_url()
 
238
        if base[-1] != '/':
 
239
            base = base + '/'
234
240
        super(SFTPTransport, self).__init__(base)
235
241
        if clone_from is None:
236
242
            self._sftp_connect()
263
269
        @param relpath: the relative path or path components
264
270
        @type relpath: str or list
265
271
        """
266
 
        return self._unparse_url(self._abspath(relpath))
 
272
        return self._unparse_url(self._remote_path(relpath))
267
273
    
268
 
    def _abspath(self, relpath):
269
 
        """Return the absolute path segment without the SFTP URL."""
 
274
    def _remote_path(self, relpath):
 
275
        """Return the path to be passed along the sftp protocol for relpath.
 
276
        
 
277
        relpath is a urlencoded string.
 
278
        """
270
279
        # FIXME: share the common code across transports
271
280
        assert isinstance(relpath, basestring)
272
 
        relpath = [urllib.unquote(relpath)]
 
281
        relpath = urllib.unquote(relpath).split('/')
273
282
        basepath = self._path.split('/')
274
283
        if len(basepath) > 0 and basepath[-1] == '':
275
284
            basepath = basepath[:-1]
287
296
                basepath.append(p)
288
297
 
289
298
        path = '/'.join(basepath)
290
 
        # could still be a "relative" path here, but relative on the sftp server
291
299
        return path
292
300
 
293
301
    def relpath(self, abspath):
305
313
            extra = ': ' + ', '.join(error)
306
314
            raise PathNotChild(abspath, self.base, extra=extra)
307
315
        pl = len(self._path)
308
 
        return path[pl:].lstrip('/')
 
316
        return path[pl:].strip('/')
309
317
 
310
318
    def has(self, relpath):
311
319
        """
312
320
        Does the target location exist?
313
321
        """
314
322
        try:
315
 
            self._sftp.stat(self._abspath(relpath))
 
323
            self._sftp.stat(self._remote_path(relpath))
316
324
            return True
317
325
        except IOError:
318
326
            return False
324
332
        :param relpath: The relative path to the file
325
333
        """
326
334
        try:
327
 
            path = self._abspath(relpath)
 
335
            path = self._remote_path(relpath)
328
336
            f = self._sftp.file(path, mode='rb')
329
337
            if self._do_prefetch and hasattr(f, 'prefetch'):
330
338
                f.prefetch()
359
367
        :param f:       File-like or string object.
360
368
        :param mode: The final mode for the file
361
369
        """
362
 
        final_path = self._abspath(relpath)
 
370
        final_path = self._remote_path(relpath)
363
371
        self._put(final_path, f, mode=mode)
364
372
 
365
373
    def _put(self, abspath, f, mode=None):
412
420
    def mkdir(self, relpath, mode=None):
413
421
        """Create a directory at the given path."""
414
422
        try:
415
 
            path = self._abspath(relpath)
 
423
            path = self._remote_path(relpath)
416
424
            # In the paramiko documentation, it says that passing a mode flag 
417
425
            # will filtered against the server umask.
418
426
            # StubSFTPServer does not do this, which would be nice, because it is
460
468
        location.
461
469
        """
462
470
        try:
463
 
            path = self._abspath(relpath)
 
471
            path = self._remote_path(relpath)
464
472
            fout = self._sftp.file(path, 'ab')
465
473
            self._pump(f, fout)
466
474
        except (IOError, paramiko.SSHException), e:
468
476
 
469
477
    def copy(self, rel_from, rel_to):
470
478
        """Copy the item at rel_from to the location at rel_to"""
471
 
        path_from = self._abspath(rel_from)
472
 
        path_to = self._abspath(rel_to)
 
479
        path_from = self._remote_path(rel_from)
 
480
        path_to = self._remote_path(rel_to)
473
481
        self._copy_abspaths(path_from, path_to)
474
482
 
475
483
    def _copy_abspaths(self, path_from, path_to, mode=None):
505
513
            total = self._get_total(relpaths)
506
514
            count = 0
507
515
            for path in relpaths:
508
 
                path_from = self._abspath(relpath)
509
 
                path_to = other._abspath(relpath)
 
516
                path_from = self._remote_path(relpath)
 
517
                path_to = other._remote_path(relpath)
510
518
                self._update_pb(pb, 'copy-to', count, total)
511
519
                self._copy_abspaths(path_from, path_to, mode=mode)
512
520
                count += 1
528
536
 
529
537
    def move(self, rel_from, rel_to):
530
538
        """Move the item at rel_from to the location at rel_to"""
531
 
        path_from = self._abspath(rel_from)
532
 
        path_to = self._abspath(rel_to)
 
539
        path_from = self._remote_path(rel_from)
 
540
        path_to = self._remote_path(rel_to)
533
541
        self._rename(path_from, path_to)
534
542
 
535
543
    def delete(self, relpath):
536
544
        """Delete the item at relpath"""
537
 
        path = self._abspath(relpath)
 
545
        path = self._remote_path(relpath)
538
546
        try:
539
547
            self._sftp.remove(path)
540
548
        except (IOError, paramiko.SSHException), e:
549
557
        Return a list of all files at the given location.
550
558
        """
551
559
        # does anything actually use this?
552
 
        path = self._abspath(relpath)
 
560
        path = self._remote_path(relpath)
553
561
        try:
554
562
            return self._sftp.listdir(path)
555
563
        except (IOError, paramiko.SSHException), e:
557
565
 
558
566
    def stat(self, relpath):
559
567
        """Return the stat information for a file."""
560
 
        path = self._abspath(relpath)
 
568
        path = self._remote_path(relpath)
561
569
        try:
562
570
            return self._sftp.stat(path)
563
571
        except (IOError, paramiko.SSHException), e:
664
672
        
665
673
        vendor = _get_ssh_vendor()
666
674
        if vendor != 'none':
667
 
            sock = SFTPSubprocess(self._host, self._port, self._username)
 
675
            sock = SFTPSubprocess(self._host, vendor, self._port,
 
676
                                  self._username)
668
677
            self._sftp = SFTPClient(sock)
669
678
        else:
670
679
            self._paramiko_connect()
744
753
        if self._try_pkey_auth(transport, paramiko.DSSKey, username, 'id_dsa'):
745
754
            return
746
755
 
747
 
 
748
756
        if self._password:
749
757
            try:
750
758
                transport.auth_password(username, self._password)
817
825
            self._translate_io_exception(e, abspath, ': unable to open',
818
826
                failure_exc=FileExists)
819
827
 
 
828
 
 
829
# ------------- server test implementation --------------
 
830
import socket
 
831
import threading
 
832
 
 
833
from bzrlib.tests.stub_sftp import StubServer, StubSFTPServer
 
834
 
 
835
STUB_SERVER_KEY = """
 
836
-----BEGIN RSA PRIVATE KEY-----
 
837
MIICWgIBAAKBgQDTj1bqB4WmayWNPB+8jVSYpZYk80Ujvj680pOTh2bORBjbIAyz
 
838
oWGW+GUjzKxTiiPvVmxFgx5wdsFvF03v34lEVVhMpouqPAYQ15N37K/ir5XY+9m/
 
839
d8ufMCkjeXsQkKqFbAlQcnWMCRnOoPHS3I4vi6hmnDDeeYTSRvfLbW0fhwIBIwKB
 
840
gBIiOqZYaoqbeD9OS9z2K9KR2atlTxGxOJPXiP4ESqP3NVScWNwyZ3NXHpyrJLa0
 
841
EbVtzsQhLn6rF+TzXnOlcipFvjsem3iYzCpuChfGQ6SovTcOjHV9z+hnpXvQ/fon
 
842
soVRZY65wKnF7IAoUwTmJS9opqgrN6kRgCd3DASAMd1bAkEA96SBVWFt/fJBNJ9H
 
843
tYnBKZGw0VeHOYmVYbvMSstssn8un+pQpUm9vlG/bp7Oxd/m+b9KWEh2xPfv6zqU
 
844
avNwHwJBANqzGZa/EpzF4J8pGti7oIAPUIDGMtfIcmqNXVMckrmzQ2vTfqtkEZsA
 
845
4rE1IERRyiJQx6EJsz21wJmGV9WJQ5kCQQDwkS0uXqVdFzgHO6S++tjmjYcxwr3g
 
846
H0CoFYSgbddOT6miqRskOQF3DZVkJT3kyuBgU2zKygz52ukQZMqxCb1fAkASvuTv
 
847
qfpH87Qq5kQhNKdbbwbmd2NxlNabazPijWuphGTdW0VfJdWfklyS2Kr+iqrs/5wV
 
848
HhathJt636Eg7oIjAkA8ht3MQ+XSl9yIJIS8gVpbPxSw5OMfw0PjVE7tBdQruiSc
 
849
nvuQES5C9BMHjF39LZiGH1iLQy7FgdHyoP+eodI7
 
850
-----END RSA PRIVATE KEY-----
 
851
"""
 
852
    
 
853
 
 
854
class SingleListener(threading.Thread):
 
855
 
 
856
    def __init__(self, callback):
 
857
        threading.Thread.__init__(self)
 
858
        self._callback = callback
 
859
        self._socket = socket.socket()
 
860
        self._socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
 
861
        self._socket.bind(('localhost', 0))
 
862
        self._socket.listen(1)
 
863
        self.port = self._socket.getsockname()[1]
 
864
        self.stop_event = threading.Event()
 
865
 
 
866
    def run(self):
 
867
        s, _ = self._socket.accept()
 
868
        # now close the listen socket
 
869
        self._socket.close()
 
870
        self._callback(s, self.stop_event)
 
871
    
 
872
    def stop(self):
 
873
        self.stop_event.set()
 
874
        # We should consider waiting for the other thread
 
875
        # to stop, because otherwise we get spurious
 
876
        #   bzr: ERROR: Socket exception: Connection reset by peer (54)
 
877
        # because the test suite finishes before the thread has a chance
 
878
        # to close. (Especially when only running a few tests)
 
879
        
 
880
        
 
881
class SFTPServer(Server):
 
882
    """Common code for SFTP server facilities."""
 
883
 
 
884
    def _get_sftp_url(self, path):
 
885
        """Calculate a sftp url to this server for path."""
 
886
        return 'sftp://foo:bar@localhost:%d/%s' % (self._listener.port, path)
 
887
 
 
888
    def __init__(self):
 
889
        self._original_vendor = None
 
890
        self._homedir = None
 
891
        self._server_homedir = None
 
892
        self._listener = None
 
893
        self._root = None
 
894
        # sftp server logs
 
895
        self.logs = []
 
896
 
 
897
    def log(self, message):
 
898
        """What to do here? do we need this? Its for the StubServer.."""
 
899
        self.logs.append(message)
 
900
 
 
901
    def _run_server(self, s, stop_event):
 
902
        ssh_server = paramiko.Transport(s)
 
903
        key_file = os.path.join(self._homedir, 'test_rsa.key')
 
904
        file(key_file, 'w').write(STUB_SERVER_KEY)
 
905
        host_key = paramiko.RSAKey.from_private_key_file(key_file)
 
906
        ssh_server.add_server_key(host_key)
 
907
        server = StubServer(self)
 
908
        ssh_server.set_subsystem_handler('sftp', paramiko.SFTPServer,
 
909
                                         StubSFTPServer, root=self._root,
 
910
                                         home=self._server_homedir)
 
911
        event = threading.Event()
 
912
        ssh_server.start_server(event, server)
 
913
        event.wait(5.0)
 
914
        stop_event.wait(30.0)
 
915
 
 
916
    def setUp(self):
 
917
        """See bzrlib.transport.Server.setUp."""
 
918
        # XXX: 20051124 jamesh
 
919
        # The tests currently pop up a password prompt when an external ssh
 
920
        # is used.  This forces the use of the paramiko implementation.
 
921
        global _ssh_vendor
 
922
        self._original_vendor = _ssh_vendor
 
923
        _ssh_vendor = 'none'
 
924
        self._homedir = os.getcwdu()
 
925
        if self._server_homedir is None:
 
926
            self._server_homedir = self._homedir
 
927
        self._root = '/'
 
928
        # FIXME WINDOWS: _root should be _server_homedir[0]:/
 
929
        self._listener = SingleListener(self._run_server)
 
930
        self._listener.setDaemon(True)
 
931
        self._listener.start()
 
932
 
 
933
    def tearDown(self):
 
934
        """See bzrlib.transport.Server.tearDown."""
 
935
        global _ssh_vendor
 
936
        self._listener.stop()
 
937
        _ssh_vendor = self._original_vendor
 
938
 
 
939
 
 
940
class SFTPAbsoluteServer(SFTPServer):
 
941
    """A test server for sftp transports, using absolute urls."""
 
942
 
 
943
    def get_url(self):
 
944
        """See bzrlib.transport.Server.get_url."""
 
945
        return self._get_sftp_url("%%2f%s" % 
 
946
                urlescape(self._homedir[1:]))
 
947
 
 
948
 
 
949
class SFTPHomeDirServer(SFTPServer):
 
950
    """A test server for sftp transports, using homedir relative urls."""
 
951
 
 
952
    def get_url(self):
 
953
        """See bzrlib.transport.Server.get_url."""
 
954
        return self._get_sftp_url("")
 
955
 
 
956
 
 
957
class SFTPSiblingAbsoluteServer(SFTPAbsoluteServer):
 
958
    """A test servere for sftp transports, using absolute urls to non-home."""
 
959
 
 
960
    def setUp(self):
 
961
        self._server_homedir = '/dev/noone/runs/tests/here'
 
962
        super(SFTPSiblingAbsoluteServer, self).setUp()
 
963
 
 
964
 
 
965
def get_test_permutations():
 
966
    """Return the permutations to be used in testing."""
 
967
    return [(SFTPTransport, SFTPAbsoluteServer),
 
968
            (SFTPTransport, SFTPHomeDirServer),
 
969
            (SFTPTransport, SFTPSiblingAbsoluteServer),
 
970
            ]