~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

Viewing changes to bzrlib/transport/sftp.py

  • Committer: Aaron Bentley
  • Date: 2006-02-22 14:39:42 UTC
  • mto: (2027.1.2 revert-subpath-56549)
  • mto: This revision was merged to the branch mainline in revision 1570.
  • Revision ID: abentley@panoramicfeedback.com-20060222143942-ae72299f2de66767
Fixed build_tree with symlinks

Show diffs side-by-side

added added

removed removed

Lines of Context:
19
19
import errno
20
20
import getpass
21
21
import os
 
22
import random
22
23
import re
23
24
import stat
 
25
import subprocess
24
26
import sys
 
27
import time
25
28
import urllib
26
29
import urlparse
27
 
import time
28
 
import random
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, ParamikoNotPresent
 
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:
43
45
    import paramiko
44
 
except ImportError:
45
 
    error('The SFTP transport requires paramiko.')
46
 
    raise
 
46
except ImportError, e:
 
47
    raise ParamikoNotPresent(e)
47
48
else:
48
49
    from paramiko.sftp import (SFTP_FLAG_WRITE, SFTP_FLAG_CREATE,
49
50
                               SFTP_FLAG_EXCL, SFTP_FLAG_TRUNC,
52
53
    from paramiko.sftp_file import SFTPFile
53
54
    from paramiko.sftp_client import SFTPClient
54
55
 
55
 
if 'sftp' not in urlparse.uses_netloc: urlparse.uses_netloc.append('sftp')
 
56
if 'sftp' not in urlparse.uses_netloc:
 
57
    urlparse.uses_netloc.append('sftp')
 
58
 
 
59
# don't use prefetch unless paramiko version >= 1.5.2 (there were bugs earlier)
 
60
_default_do_prefetch = False
 
61
if getattr(paramiko, '__version_info__', (0, 0, 0)) >= (1, 5, 2):
 
62
    _default_do_prefetch = True
56
63
 
57
64
 
58
65
_close_fds = True
61
68
    _close_fds = False
62
69
 
63
70
_ssh_vendor = None
 
71
 
64
72
def _get_ssh_vendor():
65
73
    """Find out what version of SSH is on the system."""
66
74
    global _ssh_vendor
69
77
 
70
78
    _ssh_vendor = 'none'
71
79
 
 
80
    if 'BZR_SSH' in os.environ:
 
81
        _ssh_vendor = os.environ['BZR_SSH']
 
82
        if _ssh_vendor == 'paramiko':
 
83
            _ssh_vendor = 'none'
 
84
        return _ssh_vendor
 
85
 
72
86
    try:
73
87
        p = subprocess.Popen(['ssh', '-V'],
74
88
                             close_fds=_close_fds,
99
113
 
100
114
class SFTPSubprocess:
101
115
    """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()
 
116
    def __init__(self, hostname, vendor, port=None, user=None):
104
117
        assert vendor in ['openssh', 'ssh']
105
118
        if vendor == 'openssh':
106
119
            args = ['ssh',
143
156
        self.proc.wait()
144
157
 
145
158
 
 
159
class LoopbackSFTP(object):
 
160
    """Simple wrapper for a socket that pretends to be a paramiko Channel."""
 
161
 
 
162
    def __init__(self, sock):
 
163
        self.__socket = sock
 
164
 
 
165
    def send(self, data):
 
166
        return self.__socket.send(data)
 
167
 
 
168
    def recv(self, n):
 
169
        return self.__socket.recv(n)
 
170
 
 
171
    def recv_ready(self):
 
172
        return True
 
173
 
 
174
    def close(self):
 
175
        self.__socket.close()
 
176
 
 
177
 
146
178
SYSTEM_HOSTKEYS = {}
147
179
BZR_HOSTKEYS = {}
148
180
 
152
184
# X seconds. But that requires a lot more fanciness.
153
185
_connected_hosts = weakref.WeakValueDictionary()
154
186
 
 
187
 
155
188
def load_host_keys():
156
189
    """
157
190
    Load system host keys (probably doesn't work on windows) and any
169
202
        mutter('failed to load bzr host keys: ' + str(e))
170
203
        save_host_keys()
171
204
 
 
205
 
172
206
def save_host_keys():
173
207
    """
174
208
    Save "discovered" host keys in $(config)/ssh_host_keys/.
199
233
        self.lock_path = path + '.write-lock'
200
234
        self.transport = transport
201
235
        try:
202
 
            abspath = transport._abspath(self.lock_path)
 
236
            # RBC 20060103 FIXME should we be using private methods here ?
 
237
            abspath = transport._remote_path(self.lock_path)
203
238
            self.lock_file = transport._sftp_open_exclusive(abspath)
204
239
        except FileExists:
205
240
            raise LockError('File %r already locked' % (self.path,))
207
242
    def __del__(self):
208
243
        """Should this warn, or actually try to cleanup?"""
209
244
        if self.lock_file:
210
 
            warn("SFTPLock %r not explicitly unlocked" % (self.path,))
 
245
            warning("SFTPLock %r not explicitly unlocked" % (self.path,))
211
246
            self.unlock()
212
247
 
213
248
    def unlock(self):
225
260
    """
226
261
    Transport implementation for SFTP access.
227
262
    """
228
 
    _do_prefetch = False # Right now Paramiko's prefetch support causes things to hang
 
263
    _do_prefetch = _default_do_prefetch
229
264
 
230
265
    def __init__(self, base, clone_from=None):
231
266
        assert base.startswith('sftp://')
232
267
        self._parse_url(base)
233
268
        base = self._unparse_url()
 
269
        if base[-1] != '/':
 
270
            base = base + '/'
234
271
        super(SFTPTransport, self).__init__(base)
235
272
        if clone_from is None:
236
273
            self._sftp_connect()
263
300
        @param relpath: the relative path or path components
264
301
        @type relpath: str or list
265
302
        """
266
 
        return self._unparse_url(self._abspath(relpath))
 
303
        return self._unparse_url(self._remote_path(relpath))
267
304
    
268
 
    def _abspath(self, relpath):
269
 
        """Return the absolute path segment without the SFTP URL."""
 
305
    def _remote_path(self, relpath):
 
306
        """Return the path to be passed along the sftp protocol for relpath.
 
307
        
 
308
        relpath is a urlencoded string.
 
309
        """
270
310
        # FIXME: share the common code across transports
271
311
        assert isinstance(relpath, basestring)
272
 
        relpath = [urllib.unquote(relpath)]
 
312
        relpath = urllib.unquote(relpath).split('/')
273
313
        basepath = self._path.split('/')
274
314
        if len(basepath) > 0 and basepath[-1] == '':
275
315
            basepath = basepath[:-1]
287
327
                basepath.append(p)
288
328
 
289
329
        path = '/'.join(basepath)
290
 
        # could still be a "relative" path here, but relative on the sftp server
291
330
        return path
292
331
 
293
332
    def relpath(self, abspath):
305
344
            extra = ': ' + ', '.join(error)
306
345
            raise PathNotChild(abspath, self.base, extra=extra)
307
346
        pl = len(self._path)
308
 
        return path[pl:].lstrip('/')
 
347
        return path[pl:].strip('/')
309
348
 
310
349
    def has(self, relpath):
311
350
        """
312
351
        Does the target location exist?
313
352
        """
314
353
        try:
315
 
            self._sftp.stat(self._abspath(relpath))
 
354
            self._sftp.stat(self._remote_path(relpath))
316
355
            return True
317
356
        except IOError:
318
357
            return False
324
363
        :param relpath: The relative path to the file
325
364
        """
326
365
        try:
327
 
            path = self._abspath(relpath)
 
366
            path = self._remote_path(relpath)
328
367
            f = self._sftp.file(path, mode='rb')
329
 
            if self._do_prefetch and hasattr(f, 'prefetch'):
 
368
            if self._do_prefetch and (getattr(f, 'prefetch', None) is not None):
330
369
                f.prefetch()
331
370
            return f
332
371
        except (IOError, paramiko.SSHException), e:
359
398
        :param f:       File-like or string object.
360
399
        :param mode: The final mode for the file
361
400
        """
362
 
        final_path = self._abspath(relpath)
 
401
        final_path = self._remote_path(relpath)
363
402
        self._put(final_path, f, mode=mode)
364
403
 
365
404
    def _put(self, abspath, f, mode=None):
412
451
    def mkdir(self, relpath, mode=None):
413
452
        """Create a directory at the given path."""
414
453
        try:
415
 
            path = self._abspath(relpath)
 
454
            path = self._remote_path(relpath)
416
455
            # In the paramiko documentation, it says that passing a mode flag 
417
456
            # will filtered against the server umask.
418
457
            # StubSFTPServer does not do this, which would be nice, because it is
460
499
        location.
461
500
        """
462
501
        try:
463
 
            path = self._abspath(relpath)
 
502
            path = self._remote_path(relpath)
464
503
            fout = self._sftp.file(path, 'ab')
465
504
            self._pump(f, fout)
466
505
        except (IOError, paramiko.SSHException), e:
467
506
            self._translate_io_exception(e, relpath, ': unable to append')
468
507
 
469
 
    def copy(self, rel_from, rel_to):
470
 
        """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)
473
 
        self._copy_abspaths(path_from, path_to)
474
 
 
475
 
    def _copy_abspaths(self, path_from, path_to, mode=None):
476
 
        """Copy files given an absolute path
477
 
 
478
 
        :param path_from: Path on remote server to read
479
 
        :param path_to: Path on remote server to write
480
 
        :return: None
481
 
 
482
 
        TODO: Should the destination location be atomically created?
483
 
              This has not been specified
484
 
        TODO: This should use some sort of remote copy, rather than
485
 
              pulling the data locally, and then writing it remotely
486
 
        """
487
 
        try:
488
 
            fin = self._sftp.file(path_from, 'rb')
489
 
            try:
490
 
                self._put(path_to, fin, mode=mode)
491
 
            finally:
492
 
                fin.close()
493
 
        except (IOError, paramiko.SSHException), e:
494
 
            self._translate_io_exception(e, path_from, ': unable copy to: %r' % path_to)
495
 
 
496
 
    def copy_to(self, relpaths, other, mode=None, pb=None):
497
 
        """Copy a set of entries from self into another Transport.
498
 
 
499
 
        :param relpaths: A list/generator of entries to be copied.
500
 
        """
501
 
        if isinstance(other, SFTPTransport) and other._sftp is self._sftp:
502
 
            # Both from & to are on the same remote filesystem
503
 
            # We can use a remote copy, instead of pulling locally, and pushing
504
 
 
505
 
            total = self._get_total(relpaths)
506
 
            count = 0
507
 
            for path in relpaths:
508
 
                path_from = self._abspath(relpath)
509
 
                path_to = other._abspath(relpath)
510
 
                self._update_pb(pb, 'copy-to', count, total)
511
 
                self._copy_abspaths(path_from, path_to, mode=mode)
512
 
                count += 1
513
 
            return count
514
 
        else:
515
 
            return super(SFTPTransport, self).copy_to(relpaths, other, mode=mode, pb=pb)
516
 
 
517
508
    def _rename(self, abs_from, abs_to):
518
509
        """Do a fancy rename on the remote server.
519
510
        
528
519
 
529
520
    def move(self, rel_from, rel_to):
530
521
        """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)
 
522
        path_from = self._remote_path(rel_from)
 
523
        path_to = self._remote_path(rel_to)
533
524
        self._rename(path_from, path_to)
534
525
 
535
526
    def delete(self, relpath):
536
527
        """Delete the item at relpath"""
537
 
        path = self._abspath(relpath)
 
528
        path = self._remote_path(relpath)
538
529
        try:
539
530
            self._sftp.remove(path)
540
531
        except (IOError, paramiko.SSHException), e:
549
540
        Return a list of all files at the given location.
550
541
        """
551
542
        # does anything actually use this?
552
 
        path = self._abspath(relpath)
 
543
        path = self._remote_path(relpath)
553
544
        try:
554
545
            return self._sftp.listdir(path)
555
546
        except (IOError, paramiko.SSHException), e:
556
547
            self._translate_io_exception(e, path, ': failed to list_dir')
557
548
 
 
549
    def rmdir(self, relpath):
 
550
        """See Transport.rmdir."""
 
551
        path = self._remote_path(relpath)
 
552
        try:
 
553
            return self._sftp.rmdir(path)
 
554
        except (IOError, paramiko.SSHException), e:
 
555
            self._translate_io_exception(e, path, ': failed to rmdir')
 
556
 
558
557
    def stat(self, relpath):
559
558
        """Return the stat information for a file."""
560
 
        path = self._abspath(relpath)
 
559
        path = self._remote_path(relpath)
561
560
        try:
562
561
            return self._sftp.stat(path)
563
562
        except (IOError, paramiko.SSHException), e:
589
588
        # that we have taken the lock.
590
589
        return SFTPLock(relpath, self)
591
590
 
592
 
 
593
591
    def _unparse_url(self, path=None):
594
592
        if path is None:
595
593
            path = self._path
596
594
        path = urllib.quote(path)
597
 
        if path.startswith('/'):
598
 
            path = '/%2F' + path[1:]
599
 
        else:
600
 
            path = '/' + path
 
595
        # handle homedir paths
 
596
        if not path.startswith('/'):
 
597
            path = "/~/" + path
601
598
        netloc = urllib.quote(self._host)
602
599
        if self._username is not None:
603
600
            netloc = '%s@%s' % (urllib.quote(self._username), netloc)
637
634
        # as a homedir relative path (the path begins with a double slash
638
635
        # if it is absolute).
639
636
        # see draft-ietf-secsh-scp-sftp-ssh-uri-03.txt
640
 
        if path.startswith('/'):
641
 
            path = path[1:]
642
 
 
 
637
        # RBC 20060118 we are not using this as its too user hostile. instead
 
638
        # we are following lftp and using /~/foo to mean '~/foo'.
 
639
        # handle homedir paths
 
640
        if path.startswith('/~/'):
 
641
            path = path[3:]
 
642
        elif path == '/~':
 
643
            path = ''
643
644
        return (username, password, host, port, path)
644
645
 
645
646
    def _parse_url(self, url):
663
664
            pass
664
665
        
665
666
        vendor = _get_ssh_vendor()
666
 
        if vendor != 'none':
667
 
            sock = SFTPSubprocess(self._host, self._port, self._username)
 
667
        if vendor == 'loopback':
 
668
            sock = socket.socket()
 
669
            sock.connect((self._host, self._port))
 
670
            self._sftp = SFTPClient(LoopbackSFTP(sock))
 
671
        elif vendor != 'none':
 
672
            sock = SFTPSubprocess(self._host, vendor, self._port,
 
673
                                  self._username)
668
674
            self._sftp = SFTPClient(sock)
669
675
        else:
670
676
            self._paramiko_connect()
678
684
 
679
685
        try:
680
686
            t = paramiko.Transport((self._host, self._port or 22))
 
687
            t.set_log_channel('bzr.paramiko')
681
688
            t.start_client()
682
689
        except paramiko.SSHException, e:
683
690
            raise ConnectionError('Unable to reach SSH host %s:%d' %
744
751
        if self._try_pkey_auth(transport, paramiko.DSSKey, username, 'id_dsa'):
745
752
            return
746
753
 
747
 
 
748
754
        if self._password:
749
755
            try:
750
756
                transport.auth_password(username, self._password)
817
823
            self._translate_io_exception(e, abspath, ': unable to open',
818
824
                failure_exc=FileExists)
819
825
 
 
826
 
 
827
# ------------- server test implementation --------------
 
828
import socket
 
829
import threading
 
830
 
 
831
from bzrlib.tests.stub_sftp import StubServer, StubSFTPServer
 
832
 
 
833
STUB_SERVER_KEY = """
 
834
-----BEGIN RSA PRIVATE KEY-----
 
835
MIICWgIBAAKBgQDTj1bqB4WmayWNPB+8jVSYpZYk80Ujvj680pOTh2bORBjbIAyz
 
836
oWGW+GUjzKxTiiPvVmxFgx5wdsFvF03v34lEVVhMpouqPAYQ15N37K/ir5XY+9m/
 
837
d8ufMCkjeXsQkKqFbAlQcnWMCRnOoPHS3I4vi6hmnDDeeYTSRvfLbW0fhwIBIwKB
 
838
gBIiOqZYaoqbeD9OS9z2K9KR2atlTxGxOJPXiP4ESqP3NVScWNwyZ3NXHpyrJLa0
 
839
EbVtzsQhLn6rF+TzXnOlcipFvjsem3iYzCpuChfGQ6SovTcOjHV9z+hnpXvQ/fon
 
840
soVRZY65wKnF7IAoUwTmJS9opqgrN6kRgCd3DASAMd1bAkEA96SBVWFt/fJBNJ9H
 
841
tYnBKZGw0VeHOYmVYbvMSstssn8un+pQpUm9vlG/bp7Oxd/m+b9KWEh2xPfv6zqU
 
842
avNwHwJBANqzGZa/EpzF4J8pGti7oIAPUIDGMtfIcmqNXVMckrmzQ2vTfqtkEZsA
 
843
4rE1IERRyiJQx6EJsz21wJmGV9WJQ5kCQQDwkS0uXqVdFzgHO6S++tjmjYcxwr3g
 
844
H0CoFYSgbddOT6miqRskOQF3DZVkJT3kyuBgU2zKygz52ukQZMqxCb1fAkASvuTv
 
845
qfpH87Qq5kQhNKdbbwbmd2NxlNabazPijWuphGTdW0VfJdWfklyS2Kr+iqrs/5wV
 
846
HhathJt636Eg7oIjAkA8ht3MQ+XSl9yIJIS8gVpbPxSw5OMfw0PjVE7tBdQruiSc
 
847
nvuQES5C9BMHjF39LZiGH1iLQy7FgdHyoP+eodI7
 
848
-----END RSA PRIVATE KEY-----
 
849
"""
 
850
    
 
851
 
 
852
class SingleListener(threading.Thread):
 
853
 
 
854
    def __init__(self, callback):
 
855
        threading.Thread.__init__(self)
 
856
        self._callback = callback
 
857
        self._socket = socket.socket()
 
858
        self._socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
 
859
        self._socket.bind(('localhost', 0))
 
860
        self._socket.listen(1)
 
861
        self.port = self._socket.getsockname()[1]
 
862
        self.stop_event = threading.Event()
 
863
 
 
864
    def run(self):
 
865
        s, _ = self._socket.accept()
 
866
        # now close the listen socket
 
867
        self._socket.close()
 
868
        try:
 
869
            self._callback(s, self.stop_event)
 
870
        except socket.error:
 
871
            pass #Ignore socket errors
 
872
        except Exception, x:
 
873
            # probably a failed test
 
874
            warning('Exception from within unit test server thread: %r' % x)
 
875
 
 
876
    def stop(self):
 
877
        self.stop_event.set()
 
878
        # use a timeout here, because if the test fails, the server thread may
 
879
        # never notice the stop_event.
 
880
        self.join(5.0)
 
881
 
 
882
 
 
883
class SFTPServer(Server):
 
884
    """Common code for SFTP server facilities."""
 
885
 
 
886
    def __init__(self):
 
887
        self._original_vendor = None
 
888
        self._homedir = None
 
889
        self._server_homedir = None
 
890
        self._listener = None
 
891
        self._root = None
 
892
        self._vendor = 'none'
 
893
        # sftp server logs
 
894
        self.logs = []
 
895
 
 
896
    def _get_sftp_url(self, path):
 
897
        """Calculate an sftp url to this server for path."""
 
898
        return 'sftp://foo:bar@localhost:%d/%s' % (self._listener.port, path)
 
899
 
 
900
    def log(self, message):
 
901
        """StubServer uses this to log when a new server is created."""
 
902
        self.logs.append(message)
 
903
 
 
904
    def _run_server(self, s, stop_event):
 
905
        ssh_server = paramiko.Transport(s)
 
906
        key_file = os.path.join(self._homedir, 'test_rsa.key')
 
907
        file(key_file, 'w').write(STUB_SERVER_KEY)
 
908
        host_key = paramiko.RSAKey.from_private_key_file(key_file)
 
909
        ssh_server.add_server_key(host_key)
 
910
        server = StubServer(self)
 
911
        ssh_server.set_subsystem_handler('sftp', paramiko.SFTPServer,
 
912
                                         StubSFTPServer, root=self._root,
 
913
                                         home=self._server_homedir)
 
914
        event = threading.Event()
 
915
        ssh_server.start_server(event, server)
 
916
        event.wait(5.0)
 
917
        stop_event.wait(30.0)
 
918
    
 
919
    def setUp(self):
 
920
        global _ssh_vendor
 
921
        self._original_vendor = _ssh_vendor
 
922
        _ssh_vendor = self._vendor
 
923
        self._homedir = os.getcwdu()
 
924
        if self._server_homedir is None:
 
925
            self._server_homedir = self._homedir
 
926
        self._root = '/'
 
927
        # FIXME WINDOWS: _root should be _server_homedir[0]:/
 
928
        self._listener = SingleListener(self._run_server)
 
929
        self._listener.setDaemon(True)
 
930
        self._listener.start()
 
931
 
 
932
    def tearDown(self):
 
933
        """See bzrlib.transport.Server.tearDown."""
 
934
        global _ssh_vendor
 
935
        self._listener.stop()
 
936
        _ssh_vendor = self._original_vendor
 
937
 
 
938
 
 
939
class SFTPFullAbsoluteServer(SFTPServer):
 
940
    """A test server for sftp transports, using absolute urls and ssh."""
 
941
 
 
942
    def get_url(self):
 
943
        """See bzrlib.transport.Server.get_url."""
 
944
        return self._get_sftp_url(urlescape(self._homedir[1:]))
 
945
 
 
946
 
 
947
class SFTPServerWithoutSSH(SFTPServer):
 
948
    """An SFTP server that uses a simple TCP socket pair rather than SSH."""
 
949
 
 
950
    def __init__(self):
 
951
        super(SFTPServerWithoutSSH, self).__init__()
 
952
        self._vendor = 'loopback'
 
953
 
 
954
    def _run_server(self, sock, stop_event):
 
955
        class FakeChannel(object):
 
956
            def get_transport(self):
 
957
                return self
 
958
            def get_log_channel(self):
 
959
                return 'paramiko'
 
960
            def get_name(self):
 
961
                return '1'
 
962
            def get_hexdump(self):
 
963
                return False
 
964
 
 
965
        server = paramiko.SFTPServer(FakeChannel(), 'sftp', StubServer(self), StubSFTPServer,
 
966
                                     root=self._root, home=self._server_homedir)
 
967
        server.start_subsystem('sftp', None, sock)
 
968
        server.finish_subsystem()
 
969
 
 
970
 
 
971
class SFTPAbsoluteServer(SFTPServerWithoutSSH):
 
972
    """A test server for sftp transports, using absolute urls."""
 
973
 
 
974
    def get_url(self):
 
975
        """See bzrlib.transport.Server.get_url."""
 
976
        return self._get_sftp_url(urlescape(self._homedir[1:]))
 
977
 
 
978
 
 
979
class SFTPHomeDirServer(SFTPServerWithoutSSH):
 
980
    """A test server for sftp transports, using homedir relative urls."""
 
981
 
 
982
    def get_url(self):
 
983
        """See bzrlib.transport.Server.get_url."""
 
984
        return self._get_sftp_url("~/")
 
985
 
 
986
 
 
987
class SFTPSiblingAbsoluteServer(SFTPAbsoluteServer):
 
988
    """A test servere for sftp transports, using absolute urls to non-home."""
 
989
 
 
990
    def setUp(self):
 
991
        self._server_homedir = '/dev/noone/runs/tests/here'
 
992
        super(SFTPSiblingAbsoluteServer, self).setUp()
 
993
 
 
994
 
 
995
def get_test_permutations():
 
996
    """Return the permutations to be used in testing."""
 
997
    return [(SFTPTransport, SFTPAbsoluteServer),
 
998
            (SFTPTransport, SFTPHomeDirServer),
 
999
            (SFTPTransport, SFTPSiblingAbsoluteServer),
 
1000
            ]