~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

Viewing changes to bzrlib/transport/sftp.py

  • Committer: Robert Collins
  • Date: 2007-08-05 02:57:45 UTC
  • mto: (2592.3.77 repository)
  • mto: This revision was merged to the branch mainline in revision 2741.
  • Revision ID: robertc@robertcollins.net-20070805025745-eg2qmr8jzsky39y2
StartĀ open_file_streamĀ logic.

Show diffs side-by-side

added added

removed removed

Lines of Context:
34
34
import time
35
35
import urllib
36
36
import urlparse
37
 
import weakref
38
37
 
39
38
from bzrlib import (
40
39
    errors,
48
47
                           ParamikoNotPresent,
49
48
                           )
50
49
from bzrlib.osutils import pathjoin, fancy_rename, getcwd
 
50
from bzrlib.symbol_versioning import (
 
51
        deprecated_function,
 
52
        zero_nineteen,
 
53
        )
51
54
from bzrlib.trace import mutter, warning
52
55
from bzrlib.transport import (
 
56
    _file_streams,
53
57
    local,
54
58
    register_urlparse_netloc_protocol,
55
59
    Server,
56
 
    split_url,
57
60
    ssh,
58
 
    Transport,
 
61
    ConnectedTransport,
59
62
    )
60
63
 
61
64
try:
73
76
register_urlparse_netloc_protocol('sftp')
74
77
 
75
78
 
76
 
# This is a weakref dictionary, so that we can reuse connections
77
 
# that are still active. Long term, it might be nice to have some
78
 
# sort of expiration policy, such as disconnect if inactive for
79
 
# X seconds. But that requires a lot more fanciness.
80
 
_connected_hosts = weakref.WeakValueDictionary()
81
 
 
82
 
 
83
79
_paramiko_version = getattr(paramiko, '__version_info__', (0, 0, 0))
84
80
# don't use prefetch unless paramiko version >= 1.5.5 (there were bugs earlier)
85
81
_default_do_prefetch = (_paramiko_version >= (1, 5, 5))
86
82
 
87
83
 
 
84
@deprecated_function(zero_nineteen)
88
85
def clear_connection_cache():
89
86
    """Remove all hosts from the SFTP connection cache.
90
87
 
91
88
    Primarily useful for test cases wanting to force garbage collection.
 
89
    We don't have a global connection cache anymore.
92
90
    """
93
 
    _connected_hosts.clear()
94
 
 
95
91
 
96
92
class SFTPLock(object):
97
93
    """This fakes a lock in a remote location.
135
131
            pass
136
132
 
137
133
 
138
 
class SFTPUrlHandling(Transport):
139
 
    """Mix-in that does common handling of SSH/SFTP URLs."""
140
 
 
141
 
    def __init__(self, base):
142
 
        self._parse_url(base)
143
 
        base = self._unparse_url(self._path)
144
 
        if base[-1] != '/':
145
 
            base += '/'
146
 
        super(SFTPUrlHandling, self).__init__(base)
147
 
 
148
 
    def _parse_url(self, url):
149
 
        (self._scheme,
150
 
         self._username, self._password,
151
 
         self._host, self._port, self._path) = self._split_url(url)
152
 
 
153
 
    def _unparse_url(self, path):
154
 
        """Return a URL for a path relative to this transport.
155
 
        """
156
 
        path = urllib.quote(path)
157
 
        # handle homedir paths
158
 
        if not path.startswith('/'):
159
 
            path = "/~/" + path
160
 
        netloc = urllib.quote(self._host)
161
 
        if self._username is not None:
162
 
            netloc = '%s@%s' % (urllib.quote(self._username), netloc)
163
 
        if self._port is not None:
164
 
            netloc = '%s:%d' % (netloc, self._port)
165
 
        return urlparse.urlunparse((self._scheme, netloc, path, '', '', ''))
166
 
 
167
 
    def _split_url(self, url):
168
 
        (scheme, username, password, host, port, path) = split_url(url)
169
 
        ## assert scheme == 'sftp'
170
 
 
171
 
        # the initial slash should be removed from the path, and treated
172
 
        # as a homedir relative path (the path begins with a double slash
173
 
        # if it is absolute).
174
 
        # see draft-ietf-secsh-scp-sftp-ssh-uri-03.txt
175
 
        # RBC 20060118 we are not using this as its too user hostile. instead
176
 
        # we are following lftp and using /~/foo to mean '~/foo'.
177
 
        # handle homedir paths
178
 
        if path.startswith('/~/'):
179
 
            path = path[3:]
180
 
        elif path == '/~':
181
 
            path = ''
182
 
        return (scheme, username, password, host, port, path)
183
 
 
184
 
    def abspath(self, relpath):
185
 
        """Return the full url to the given relative path.
186
 
        
187
 
        @param relpath: the relative path or path components
188
 
        @type relpath: str or list
189
 
        """
190
 
        return self._unparse_url(self._remote_path(relpath))
191
 
    
192
 
    def _remote_path(self, relpath):
193
 
        """Return the path to be passed along the sftp protocol for relpath.
194
 
        
195
 
        :param relpath: is a urlencoded string.
196
 
        """
197
 
        return self._combine_paths(self._path, relpath)
198
 
 
199
 
 
200
 
class SFTPTransport(SFTPUrlHandling):
 
134
class SFTPTransport(ConnectedTransport):
201
135
    """Transport implementation for SFTP access."""
202
136
 
203
137
    _do_prefetch = _default_do_prefetch
218
152
    # up the request itself, rather than us having to worry about it
219
153
    _max_request_size = 32768
220
154
 
221
 
    def __init__(self, base, clone_from=None):
222
 
        super(SFTPTransport, self).__init__(base)
223
 
        if clone_from is None:
224
 
            self._sftp_connect()
 
155
    def __init__(self, base, _from_transport=None):
 
156
        assert base.startswith('sftp://')
 
157
        super(SFTPTransport, self).__init__(base,
 
158
                                            _from_transport=_from_transport)
 
159
 
 
160
    def close_file_stream(self, relpath):
 
161
        """See Transport.close_file_stream."""
 
162
        handle = _file_streams.pop(self.abspath(relpath))
 
163
        handle.close()
 
164
 
 
165
    def _remote_path(self, relpath):
 
166
        """Return the path to be passed along the sftp protocol for relpath.
 
167
        
 
168
        :param relpath: is a urlencoded string.
 
169
        """
 
170
        relative = urlutils.unescape(relpath).encode('utf-8')
 
171
        remote_path = self._combine_paths(self._path, relative)
 
172
        # the initial slash should be removed from the path, and treated as a
 
173
        # homedir relative path (the path begins with a double slash if it is
 
174
        # absolute).  see draft-ietf-secsh-scp-sftp-ssh-uri-03.txt
 
175
        # RBC 20060118 we are not using this as its too user hostile. instead
 
176
        # we are following lftp and using /~/foo to mean '~/foo'
 
177
        # vila--20070602 and leave absolute paths begin with a single slash.
 
178
        if remote_path.startswith('/~/'):
 
179
            remote_path = remote_path[3:]
 
180
        elif remote_path == '/~':
 
181
            remote_path = ''
 
182
        return remote_path
 
183
 
 
184
    def _create_connection(self, credentials=None):
 
185
        """Create a new connection with the provided credentials.
 
186
 
 
187
        :param credentials: The credentials needed to establish the connection.
 
188
 
 
189
        :return: The created connection and its associated credentials.
 
190
 
 
191
        The credentials are only the password as it may have been entered
 
192
        interactively by the user and may be different from the one provided
 
193
        in base url at transport creation time.
 
194
        """
 
195
        if credentials is None:
 
196
            password = self._password
225
197
        else:
226
 
            # use the same ssh connection, etc
227
 
            self._sftp = clone_from._sftp
228
 
        # super saves 'self.base'
229
 
    
 
198
            password = credentials
 
199
 
 
200
        vendor = ssh._get_ssh_vendor()
 
201
        connection = vendor.connect_sftp(self._user, password,
 
202
                                         self._host, self._port)
 
203
        return connection, password
 
204
 
 
205
    def _get_sftp(self):
 
206
        """Ensures that a connection is established"""
 
207
        connection = self._get_connection()
 
208
        if connection is None:
 
209
            # First connection ever
 
210
            connection, credentials = self._create_connection()
 
211
            self._set_connection(connection, credentials)
 
212
        return connection
 
213
 
 
214
 
230
215
    def should_cache(self):
231
216
        """
232
217
        Return True if the data pulled across should be cached locally.
233
218
        """
234
219
        return True
235
220
 
236
 
    def clone(self, offset=None):
237
 
        """
238
 
        Return a new SFTPTransport with root at self.base + offset.
239
 
        We share the same SFTP session between such transports, because it's
240
 
        fairly expensive to set them up.
241
 
        """
242
 
        if offset is None:
243
 
            return SFTPTransport(self.base, self)
244
 
        else:
245
 
            return SFTPTransport(self.abspath(offset), self)
246
 
 
247
 
    def _remote_path(self, relpath):
248
 
        """Return the path to be passed along the sftp protocol for relpath.
249
 
        
250
 
        relpath is a urlencoded string.
251
 
 
252
 
        :return: a path prefixed with / for regular abspath-based urls, or a
253
 
            path that does not begin with / for urls which begin with /~/.
254
 
        """
255
 
        # how does this work? 
256
 
        # it processes relpath with respect to 
257
 
        # our state:
258
 
        # firstly we create a path to evaluate: 
259
 
        # if relpath is an abspath or homedir path, its the entire thing
260
 
        # otherwise we join our base with relpath
261
 
        # then we eliminate all empty segments (double //'s) outside the first
262
 
        # two elements of the list. This avoids problems with trailing 
263
 
        # slashes, or other abnormalities.
264
 
        # finally we evaluate the entire path in a single pass
265
 
        # '.'s are stripped,
266
 
        # '..' result in popping the left most already 
267
 
        # processed path (which can never be empty because of the check for
268
 
        # abspath and homedir meaning that its not, or that we've used our
269
 
        # path. If the pop would pop the root, we ignore it.
270
 
 
271
 
        # Specific case examinations:
272
 
        # remove the special casefor ~: if the current root is ~/ popping of it
273
 
        # = / thus our seed for a ~ based path is ['', '~']
274
 
        # and if we end up with [''] then we had basically ('', '..') (which is
275
 
        # '/..' so we append '' if the length is one, and assert that the first
276
 
        # element is still ''. Lastly, if we end with ['', '~'] as a prefix for
277
 
        # the output, we've got a homedir path, so we strip that prefix before
278
 
        # '/' joining the resulting list.
279
 
        #
280
 
        # case one: '/' -> ['', ''] cannot shrink
281
 
        # case two: '/' + '../foo' -> ['', 'foo'] (take '', '', '..', 'foo')
282
 
        #           and pop the second '' for the '..', append 'foo'
283
 
        # case three: '/~/' -> ['', '~', ''] 
284
 
        # case four: '/~/' + '../foo' -> ['', '~', '', '..', 'foo'],
285
 
        #           and we want to get '/foo' - the empty path in the middle
286
 
        #           needs to be stripped, then normal path manipulation will 
287
 
        #           work.
288
 
        # case five: '/..' ['', '..'], we want ['', '']
289
 
        #            stripping '' outside the first two is ok
290
 
        #            ignore .. if its too high up
291
 
        #
292
 
        # lastly this code is possibly reusable by FTP, but not reusable by
293
 
        # local paths: ~ is resolvable correctly, nor by HTTP or the smart
294
 
        # server: ~ is resolved remotely.
295
 
        # 
296
 
        # however, a version of this that acts on self.base is possible to be
297
 
        # written which manipulates the URL in canonical form, and would be
298
 
        # reusable for all transports, if a flag for allowing ~/ at all was
299
 
        # provided.
300
 
        assert isinstance(relpath, basestring)
301
 
        relpath = urlutils.unescape(relpath)
302
 
 
303
 
        # case 1)
304
 
        if relpath.startswith('/'):
305
 
            # abspath - normal split is fine.
306
 
            current_path = relpath.split('/')
307
 
        elif relpath.startswith('~/'):
308
 
            # root is homedir based: normal split and prefix '' to remote the
309
 
            # special case
310
 
            current_path = [''].extend(relpath.split('/'))
311
 
        else:
312
 
            # root is from the current directory:
313
 
            if self._path.startswith('/'):
314
 
                # abspath, take the regular split
315
 
                current_path = []
316
 
            else:
317
 
                # homedir based, add the '', '~' not present in self._path
318
 
                current_path = ['', '~']
319
 
            # add our current dir
320
 
            current_path.extend(self._path.split('/'))
321
 
            # add the users relpath
322
 
            current_path.extend(relpath.split('/'))
323
 
        # strip '' segments that are not in the first one - the leading /.
324
 
        to_process = current_path[:1]
325
 
        for segment in current_path[1:]:
326
 
            if segment != '':
327
 
                to_process.append(segment)
328
 
 
329
 
        # process '.' and '..' segments into output_path.
330
 
        output_path = []
331
 
        for segment in to_process:
332
 
            if segment == '..':
333
 
                # directory pop. Remove a directory 
334
 
                # as long as we are not at the root
335
 
                if len(output_path) > 1:
336
 
                    output_path.pop()
337
 
                # else: pass
338
 
                # cannot pop beyond the root, so do nothing
339
 
            elif segment == '.':
340
 
                continue # strip the '.' from the output.
341
 
            else:
342
 
                # this will append '' to output_path for the root elements,
343
 
                # which is appropriate: its why we strip '' in the first pass.
344
 
                output_path.append(segment)
345
 
 
346
 
        # check output special cases:
347
 
        if output_path == ['']:
348
 
            # [''] -> ['', '']
349
 
            output_path = ['', '']
350
 
        elif output_path[:2] == ['', '~']:
351
 
            # ['', '~', ...] -> ...
352
 
            output_path = output_path[2:]
353
 
        path = '/'.join(output_path)
354
 
        return path
355
 
 
356
 
    def relpath(self, abspath):
357
 
        scheme, username, password, host, port, path = self._split_url(abspath)
358
 
        error = []
359
 
        if (username != self._username):
360
 
            error.append('username mismatch')
361
 
        if (host != self._host):
362
 
            error.append('host mismatch')
363
 
        if (port != self._port):
364
 
            error.append('port mismatch')
365
 
        if (not path.startswith(self._path)):
366
 
            error.append('path mismatch')
367
 
        if error:
368
 
            extra = ': ' + ', '.join(error)
369
 
            raise PathNotChild(abspath, self.base, extra=extra)
370
 
        pl = len(self._path)
371
 
        return path[pl:].strip('/')
372
 
 
373
221
    def has(self, relpath):
374
222
        """
375
223
        Does the target location exist?
376
224
        """
377
225
        try:
378
 
            self._sftp.stat(self._remote_path(relpath))
 
226
            self._get_sftp().stat(self._remote_path(relpath))
379
227
            return True
380
228
        except IOError:
381
229
            return False
388
236
        """
389
237
        try:
390
238
            path = self._remote_path(relpath)
391
 
            f = self._sftp.file(path, mode='rb')
 
239
            f = self._get_sftp().file(path, mode='rb')
392
240
            if self._do_prefetch and (getattr(f, 'prefetch', None) is not None):
393
241
                f.prefetch()
394
242
            return f
406
254
 
407
255
        try:
408
256
            path = self._remote_path(relpath)
409
 
            fp = self._sftp.file(path, mode='rb')
 
257
            fp = self._get_sftp().file(path, mode='rb')
410
258
            readv = getattr(fp, 'readv', None)
411
259
            if readv:
412
260
                return self._sftp_readv(fp, offsets, relpath)
415
263
        except (IOError, paramiko.SSHException), e:
416
264
            self._translate_io_exception(e, path, ': error retrieving')
417
265
 
 
266
    def recommended_page_size(self):
 
267
        """See Transport.recommended_page_size().
 
268
 
 
269
        For SFTP we suggest a large page size to reduce the overhead
 
270
        introduced by latency.
 
271
        """
 
272
        return 64 * 1024
 
273
 
418
274
    def _sftp_readv(self, fp, offsets, relpath='<unknown>'):
419
275
        """Use the readv() member of fp to do async readv.
420
276
 
555
411
            # Because we set_pipelined() earlier, theoretically we might 
556
412
            # avoid the round trip for fout.close()
557
413
            if mode is not None:
558
 
                self._sftp.chmod(tmp_abspath, mode)
 
414
                self._get_sftp().chmod(tmp_abspath, mode)
559
415
            fout.close()
560
416
            closed = True
561
417
            self._rename_and_overwrite(tmp_abspath, abspath)
570
426
            try:
571
427
                if not closed:
572
428
                    fout.close()
573
 
                self._sftp.remove(tmp_abspath)
 
429
                self._get_sftp().remove(tmp_abspath)
574
430
            except:
575
431
                # raise the saved except
576
432
                raise e
591
447
            fout = None
592
448
            try:
593
449
                try:
594
 
                    fout = self._sftp.file(abspath, mode='wb')
 
450
                    fout = self._get_sftp().file(abspath, mode='wb')
595
451
                    fout.set_pipelined(True)
596
452
                    writer(fout)
597
453
                except (paramiko.SSHException, IOError), e:
602
458
                # Because we set_pipelined() earlier, theoretically we might 
603
459
                # avoid the round trip for fout.close()
604
460
                if mode is not None:
605
 
                    self._sftp.chmod(abspath, mode)
 
461
                    self._get_sftp().chmod(abspath, mode)
606
462
            finally:
607
463
                if fout is not None:
608
464
                    fout.close()
672
528
        else:
673
529
            local_mode = mode
674
530
        try:
675
 
            self._sftp.mkdir(abspath, local_mode)
 
531
            self._get_sftp().mkdir(abspath, local_mode)
676
532
            if mode is not None:
677
 
                self._sftp.chmod(abspath, mode=mode)
 
533
                self._get_sftp().chmod(abspath, mode=mode)
678
534
        except (paramiko.SSHException, IOError), e:
679
535
            self._translate_io_exception(e, abspath, ': unable to mkdir',
680
536
                failure_exc=FileExists)
683
539
        """Create a directory at the given path."""
684
540
        self._mkdir(self._remote_path(relpath), mode=mode)
685
541
 
 
542
    def open_file_stream(self, relpath):
 
543
        """See Transport.open_file_stream."""
 
544
        # initialise the file to zero-length
 
545
        # this is three round trips, but we don't use this 
 
546
        # api more than once per write_group at the moment so 
 
547
        # it is a tolerable overhead. Better would be to truncate
 
548
        # the file after opening. RBC 20070805
 
549
        self.put_bytes_non_atomic(relpath, "")
 
550
        abspath = self._remote_path(relpath)
 
551
        # TODO: jam 20060816 paramiko doesn't publicly expose a way to
 
552
        #       set the file mode at create time. If it does, use it.
 
553
        #       But for now, we just chmod later anyway.
 
554
        handle = None
 
555
        try:
 
556
            handle = self._get_sftp().file(abspath, mode='wb')
 
557
            handle.set_pipelined(True)
 
558
        except (paramiko.SSHException, IOError), e:
 
559
            self._translate_io_exception(e, abspath,
 
560
                                         ': unable to open')
 
561
        _file_streams[self.abspath(relpath)] = handle
 
562
        return handle.write
 
563
 
686
564
    def _translate_io_exception(self, e, path, more_info='',
687
565
                                failure_exc=PathError):
688
566
        """Translate a paramiko or IOError into a friendlier exception.
720
598
        """
721
599
        try:
722
600
            path = self._remote_path(relpath)
723
 
            fout = self._sftp.file(path, 'ab')
 
601
            fout = self._get_sftp().file(path, 'ab')
724
602
            if mode is not None:
725
 
                self._sftp.chmod(path, mode)
 
603
                self._get_sftp().chmod(path, mode)
726
604
            result = fout.tell()
727
605
            self._pump(f, fout)
728
606
            return result
732
610
    def rename(self, rel_from, rel_to):
733
611
        """Rename without special overwriting"""
734
612
        try:
735
 
            self._sftp.rename(self._remote_path(rel_from),
 
613
            self._get_sftp().rename(self._remote_path(rel_from),
736
614
                              self._remote_path(rel_to))
737
615
        except (IOError, paramiko.SSHException), e:
738
616
            self._translate_io_exception(e, rel_from,
744
622
        Using the implementation provided by osutils.
745
623
        """
746
624
        try:
 
625
            sftp = self._get_sftp()
747
626
            fancy_rename(abs_from, abs_to,
748
 
                    rename_func=self._sftp.rename,
749
 
                    unlink_func=self._sftp.remove)
 
627
                         rename_func=sftp.rename,
 
628
                         unlink_func=sftp.remove)
750
629
        except (IOError, paramiko.SSHException), e:
751
 
            self._translate_io_exception(e, abs_from, ': unable to rename to %r' % (abs_to))
 
630
            self._translate_io_exception(e, abs_from,
 
631
                                         ': unable to rename to %r' % (abs_to))
752
632
 
753
633
    def move(self, rel_from, rel_to):
754
634
        """Move the item at rel_from to the location at rel_to"""
760
640
        """Delete the item at relpath"""
761
641
        path = self._remote_path(relpath)
762
642
        try:
763
 
            self._sftp.remove(path)
 
643
            self._get_sftp().remove(path)
764
644
        except (IOError, paramiko.SSHException), e:
765
645
            self._translate_io_exception(e, path, ': unable to delete')
766
646
            
 
647
    def external_url(self):
 
648
        """See bzrlib.transport.Transport.external_url."""
 
649
        # the external path for SFTP is the base
 
650
        return self.base
 
651
 
767
652
    def listable(self):
768
653
        """Return True if this store supports listing."""
769
654
        return True
778
663
        # -- David Allouche 2006-08-11
779
664
        path = self._remote_path(relpath)
780
665
        try:
781
 
            entries = self._sftp.listdir(path)
 
666
            entries = self._get_sftp().listdir(path)
782
667
        except (IOError, paramiko.SSHException), e:
783
668
            self._translate_io_exception(e, path, ': failed to list_dir')
784
669
        return [urlutils.escape(entry) for entry in entries]
787
672
        """See Transport.rmdir."""
788
673
        path = self._remote_path(relpath)
789
674
        try:
790
 
            return self._sftp.rmdir(path)
 
675
            return self._get_sftp().rmdir(path)
791
676
        except (IOError, paramiko.SSHException), e:
792
677
            self._translate_io_exception(e, path, ': failed to rmdir')
793
678
 
795
680
        """Return the stat information for a file."""
796
681
        path = self._remote_path(relpath)
797
682
        try:
798
 
            return self._sftp.stat(path)
 
683
            return self._get_sftp().stat(path)
799
684
        except (IOError, paramiko.SSHException), e:
800
685
            self._translate_io_exception(e, path, ': unable to stat')
801
686
 
825
710
        # that we have taken the lock.
826
711
        return SFTPLock(relpath, self)
827
712
 
828
 
    def _sftp_connect(self):
829
 
        """Connect to the remote sftp server.
830
 
        After this, self._sftp should have a valid connection (or
831
 
        we raise an TransportError 'could not connect').
832
 
 
833
 
        TODO: Raise a more reasonable ConnectionFailed exception
834
 
        """
835
 
        self._sftp = _sftp_connect(self._host, self._port, self._username,
836
 
                self._password)
837
 
 
838
713
    def _sftp_open_exclusive(self, abspath, mode=None):
839
714
        """Open a remote path exclusively.
840
715
 
853
728
        #       using the 'x' flag to indicate SFTP_FLAG_EXCL.
854
729
        #       However, there is no way to set the permission mode at open 
855
730
        #       time using the sftp_client.file() functionality.
856
 
        path = self._sftp._adjust_cwd(abspath)
 
731
        path = self._get_sftp()._adjust_cwd(abspath)
857
732
        # mutter('sftp abspath %s => %s', abspath, path)
858
733
        attr = SFTPAttributes()
859
734
        if mode is not None:
861
736
        omode = (SFTP_FLAG_WRITE | SFTP_FLAG_CREATE 
862
737
                | SFTP_FLAG_TRUNC | SFTP_FLAG_EXCL)
863
738
        try:
864
 
            t, msg = self._sftp._request(CMD_OPEN, path, omode, attr)
 
739
            t, msg = self._get_sftp()._request(CMD_OPEN, path, omode, attr)
865
740
            if t != CMD_HANDLE:
866
741
                raise TransportError('Expected an SFTP handle')
867
742
            handle = msg.get_string()
868
 
            return SFTPFile(self._sftp, handle, 'wb', -1)
 
743
            return SFTPFile(self._get_sftp(), handle, 'wb', -1)
869
744
        except (paramiko.SSHException, IOError), e:
870
745
            self._translate_io_exception(e, abspath, ': unable to open',
871
746
                failure_exc=FileExists)
1197
1072
        super(SFTPSiblingAbsoluteServer, self).setUp(backing_server)
1198
1073
 
1199
1074
 
1200
 
def _sftp_connect(host, port, username, password):
1201
 
    """Connect to the remote sftp server.
1202
 
 
1203
 
    :raises: a TransportError 'could not connect'.
1204
 
 
1205
 
    :returns: an paramiko.sftp_client.SFTPClient
1206
 
 
1207
 
    TODO: Raise a more reasonable ConnectionFailed exception
1208
 
    """
1209
 
    idx = (host, port, username)
1210
 
    try:
1211
 
        return _connected_hosts[idx]
1212
 
    except KeyError:
1213
 
        pass
1214
 
    
1215
 
    sftp = _sftp_connect_uncached(host, port, username, password)
1216
 
    _connected_hosts[idx] = sftp
1217
 
    return sftp
1218
 
 
1219
 
def _sftp_connect_uncached(host, port, username, password):
1220
 
    vendor = ssh._get_ssh_vendor()
1221
 
    sftp = vendor.connect_sftp(username, password, host, port)
1222
 
    return sftp
1223
 
 
1224
 
 
1225
1075
def get_test_permutations():
1226
1076
    """Return the permutations to be used in testing."""
1227
1077
    return [(SFTPTransport, SFTPAbsoluteServer),