1
# Copyright (C) 2005-2010 Canonical Ltd
1
# Copyright (C) 2005-2011, 2016, 2017 Canonical Ltd
3
3
# This program is free software; you can redistribute it and/or modify
4
4
# it under the terms of the GNU General Public License as published by
17
17
"""Implementation of Transport over SFTP, using paramiko."""
19
from __future__ import absolute_import
19
21
# TODO: Remove the transport-based lock_read and lock_write methods. They'll
20
22
# then raise TransportNotPossible, which will break remote access to any
21
23
# formats which rely on OS-level locks. That should be fine as those formats
44
from bzrlib.errors import (FileExists,
45
NoSuchFile, PathNotChild,
44
from ..errors import (FileExists,
49
49
ParamikoNotPresent,
51
from bzrlib.osutils import pathjoin, fancy_rename, getcwd
52
from bzrlib.symbol_versioning import (
55
from bzrlib.trace import mutter, warning
56
from bzrlib.transport import (
51
from ..osutils import fancy_rename
52
from ..sixish import (
55
from ..trace import mutter, warning
56
from ..transport import (
62
60
ConnectedTransport,
80
except ImportError, e:
78
except ImportError as e:
81
79
raise ParamikoNotPresent(e)
83
81
from paramiko.sftp import (SFTP_FLAG_WRITE, SFTP_FLAG_CREATE,
84
82
SFTP_FLAG_EXCL, SFTP_FLAG_TRUNC,
85
SFTP_OK, CMD_HANDLE, CMD_OPEN)
86
84
from paramiko.sftp_attr import SFTPAttributes
87
85
from paramiko.sftp_file import SFTPFile
90
_paramiko_version = getattr(paramiko, '__version_info__', (0, 0, 0))
91
# don't use prefetch unless paramiko version >= 1.5.5 (there were bugs earlier)
92
_default_do_prefetch = (_paramiko_version >= (1, 5, 5))
88
# GZ 2017-05-25: Some dark hackery to monkeypatch out issues with paramiko's
89
# Python 3 compatibility code. Replace broken b() and asbytes() code.
91
from paramiko.py3compat import b as _bad
92
from paramiko.common import asbytes as _bad_asbytes
96
def _b_for_broken_paramiko(s, encoding='utf8'):
97
"""Hacked b() that does not raise TypeError."""
98
# https://github.com/paramiko/paramiko/issues/967
99
if not isinstance(s, bytes):
100
encode = getattr(s, 'encode', None)
101
if encode is not None:
102
return encode(encoding)
103
# Would like to pass buffer objects along, but have to realise.
104
tobytes = getattr(s, 'tobytes', None)
105
if tobytes is not None:
109
def _asbytes_for_broken_paramiko(s):
110
"""Hacked asbytes() that does not raise Exception."""
111
# https://github.com/paramiko/paramiko/issues/968
112
if not isinstance(s, bytes):
113
encode = getattr(s, 'encode', None)
114
if encode is not None:
115
return encode('utf8')
116
asbytes = getattr(s, 'asbytes', None)
117
if asbytes is not None:
121
_bad.__code__ = _b_for_broken_paramiko.__code__
122
_bad_asbytes.__code__ = _asbytes_for_broken_paramiko.__code__
95
125
class SFTPLock(object):
114
144
except FileExists:
115
145
raise LockError('File %r already locked' % (self.path,))
118
"""Should this warn, or actually try to cleanup?"""
120
warning("SFTPLock %r not explicitly unlocked" % (self.path,))
123
147
def unlock(self):
124
148
if not self.lock_file:
197
221
requests = self._get_requests()
198
222
offset_iter = iter(self.original_offsets)
199
cur_offset, cur_size = offset_iter.next()
223
cur_offset, cur_size = next(offset_iter)
200
224
# paramiko .readv() yields strings that are in the order of the requests
201
225
# So we track the current request to know where the next data is
202
226
# being returned from.
214
238
data_stream = itertools.chain(fp.readv(requests),
215
239
itertools.repeat(None))
216
for (start, length), data in itertools.izip(requests, data_stream):
240
for (start, length), data in zip(requests, data_stream):
218
242
if cur_coalesced is not None:
219
243
raise errors.ShortReadvError(self.relpath,
238
262
if buffered_len > 0:
239
263
# We haven't consumed the buffer so far, so put it into
240
264
# data_chunks, and continue.
241
buffered = ''.join(buffered_data)
265
buffered = b''.join(buffered_data)
242
266
data_chunks.append((input_start, buffered))
243
267
input_start = start
244
268
buffered_data = [data]
249
273
# into a single string. We also have the nice property that
250
274
# when there is only one string ''.join([x]) == x, so there is
251
275
# no data copying.
252
buffered = ''.join(buffered_data)
276
buffered = b''.join(buffered_data)
253
277
# Clean out buffered data so that we keep memory
254
278
# consumption low
255
279
del buffered_data[:]
270
294
input_start += cur_size
271
295
# Yield the requested data
272
296
yield cur_offset, cur_data
273
cur_offset, cur_size = offset_iter.next()
298
cur_offset, cur_size = next(offset_iter)
299
except StopIteration:
274
301
# at this point, we've consumed as much of buffered as we can,
275
302
# so break off the portion that we consumed
276
303
if buffered_offset == len(buffered_data):
281
308
buffered = buffered[buffered_offset:]
282
309
buffered_data = [buffered]
283
310
buffered_len = len(buffered)
311
# now that the data stream is done, close the handle
285
buffered = ''.join(buffered_data)
314
buffered = b''.join(buffered_data)
286
315
del buffered_data[:]
287
316
data_chunks.append((input_start, buffered))
289
318
if 'sftp' in debug.debug_flags:
290
319
mutter('SFTP readv left with %d out-of-order bytes',
291
sum(map(lambda x: len(x[1]), data_chunks)))
320
sum(len(x[1]) for x in data_chunks))
292
321
# We've processed all the readv data, at this point, anything we
293
322
# couldn't process is in data_chunks. This doesn't happen often, so
294
323
# this code path isn't optimized
317
346
' We expected %d bytes, but only found %d'
318
347
% (cur_size, len(data)))
319
348
yield cur_offset, data
320
cur_offset, cur_size = offset_iter.next()
350
cur_offset, cur_size = next(offset_iter)
351
except StopIteration:
323
355
class SFTPTransport(ConnectedTransport):
324
356
"""Transport implementation for SFTP access."""
326
_do_prefetch = _default_do_prefetch
327
358
# TODO: jam 20060717 Conceivably these could be configurable, either
328
359
# by auto-tuning at run-time, or by a configuration (per host??)
329
360
# but the performance curve is pretty flat, so just going with
341
372
# up the request itself, rather than us having to worry about it
342
373
_max_request_size = 32768
344
def __init__(self, base, _from_transport=None):
345
super(SFTPTransport, self).__init__(base,
346
_from_transport=_from_transport)
348
375
def _remote_path(self, relpath):
349
376
"""Return the path to be passed along the sftp protocol for relpath.
351
378
:param relpath: is a urlencoded string.
353
relative = urlutils.unescape(relpath).encode('utf-8')
354
remote_path = self._combine_paths(self._path, relative)
380
remote_path = self._parsed_url.clone(relpath).path
355
381
# the initial slash should be removed from the path, and treated as a
356
382
# homedir relative path (the path begins with a double slash if it is
357
383
# absolute). see draft-ietf-secsh-scp-sftp-ssh-uri-03.txt
376
402
in base url at transport creation time.
378
404
if credentials is None:
379
password = self._password
405
password = self._parsed_url.password
381
407
password = credentials
383
409
vendor = ssh._get_ssh_vendor()
410
user = self._parsed_url.user
386
412
auth = config.AuthenticationConfig()
387
user = auth.get_user('ssh', self._host, self._port)
388
connection = vendor.connect_sftp(self._user, password,
389
self._host, self._port)
413
user = auth.get_user('ssh', self._parsed_url.host,
414
self._parsed_url.port)
415
connection = vendor.connect_sftp(self._parsed_url.user, password,
416
self._parsed_url.host, self._parsed_url.port)
390
417
return connection, (user, password)
419
def disconnect(self):
420
connection = self._get_connection()
421
if connection is not None:
392
424
def _get_sftp(self):
393
425
"""Ensures that a connection is established"""
394
426
connection = self._get_connection()
416
448
:param relpath: The relative path to the file
419
# FIXME: by returning the file directly, we don't pass this
420
# through to report_activity. We could try wrapping the object
421
# before it's returned. For readv and get_bytes it's handled in
422
# the higher-level function.
424
451
path = self._remote_path(relpath)
425
452
f = self._get_sftp().file(path, mode='rb')
426
if self._do_prefetch and (getattr(f, 'prefetch', None) is not None):
453
size = f.stat().st_size
454
if getattr(f, 'prefetch', None) is not None:
429
except (IOError, paramiko.SSHException), e:
457
except (IOError, paramiko.SSHException) as e:
430
458
self._translate_io_exception(e, path, ': error retrieving',
431
459
failure_exc=errors.ReadError)
433
461
def get_bytes(self, relpath):
434
462
# reimplement this here so that we can report how many bytes came back
435
f = self.get(relpath)
463
with self.get(relpath) as f:
438
465
self._report_activity(len(bytes), 'read')
443
468
def _readv(self, relpath, offsets):
444
469
"""See Transport.readv()"""
457
482
if 'sftp' in debug.debug_flags:
458
483
mutter('seek and read %s offsets', len(offsets))
459
484
return self._seek_and_read(fp, offsets, relpath)
460
except (IOError, paramiko.SSHException), e:
485
except (IOError, paramiko.SSHException) as e:
461
486
self._translate_io_exception(e, path, ': error retrieving')
463
488
def recommended_page_size(self):
492
517
def _put(self, abspath, f, mode=None):
493
518
"""Helper function so both put() and copy_abspaths can reuse the code"""
494
519
tmp_abspath = '%s.tmp.%.9f.%d.%d' % (abspath, time.time(),
495
os.getpid(), random.randint(0,0x7FFFFFFF))
520
os.getpid(), random.randint(0, 0x7FFFFFFF))
496
521
fout = self._sftp_open_exclusive(tmp_abspath, mode=mode)
500
525
fout.set_pipelined(True)
501
526
length = self._pump(f, fout)
502
except (IOError, paramiko.SSHException), e:
527
except (IOError, paramiko.SSHException) as e:
503
528
self._translate_io_exception(e, tmp_abspath)
504
529
# XXX: This doesn't truly help like we would like it to.
505
530
# The problem is that openssh strips sticky bits. So while we
521
546
self._rename_and_overwrite(tmp_abspath, abspath)
548
except Exception as e:
524
549
# If we fail, try to clean up the temporary file
525
550
# before we throw the exception
526
551
# but don't let another exception mess things up
555
580
fout = self._get_sftp().file(abspath, mode='wb')
556
581
fout.set_pipelined(True)
558
except (paramiko.SSHException, IOError), e:
583
except (paramiko.SSHException, IOError) as e:
559
584
self._translate_io_exception(e, abspath,
560
585
': unable to open')
606
631
create_parent_dir=create_parent_dir,
607
632
dir_mode=dir_mode)
609
def put_bytes_non_atomic(self, relpath, bytes, mode=None,
634
def put_bytes_non_atomic(self, relpath, raw_bytes, mode=None,
610
635
create_parent_dir=False,
637
if not isinstance(raw_bytes, bytes):
639
'raw_bytes must be a plain string, not %s' % type(raw_bytes))
612
641
def writer(fout):
642
fout.write(raw_bytes)
614
643
self._put_non_atomic_helper(relpath, writer, mode=mode,
615
644
create_parent_dir=create_parent_dir,
616
645
dir_mode=dir_mode)
644
673
# the sgid bit is set, report a warning to the user
645
674
# with the umask fix.
646
675
stat = self._get_sftp().lstat(abspath)
647
mode = mode & 0777 # can't set special bits anyway
648
if mode != stat.st_mode & 0777:
649
if stat.st_mode & 06000:
676
mode = mode & 0o777 # can't set special bits anyway
677
if mode != stat.st_mode & 0o777:
678
if stat.st_mode & 0o6000:
650
679
warning('About to chmod %s over sftp, which will result'
651
680
' in its suid or sgid bits being cleared. If'
652
681
' you want to preserve those bits, change your '
653
682
' environment on the server to use umask 0%03o.'
654
% (abspath, 0777 - mode))
683
% (abspath, 0o777 - mode))
655
684
self._get_sftp().chmod(abspath, mode=mode)
656
except (paramiko.SSHException, IOError), e:
685
except (paramiko.SSHException, IOError) as e:
657
686
self._translate_io_exception(e, abspath, ': unable to mkdir',
658
687
failure_exc=FileExists)
668
697
# api more than once per write_group at the moment so
669
698
# it is a tolerable overhead. Better would be to truncate
670
699
# the file after opening. RBC 20070805
671
self.put_bytes_non_atomic(relpath, "", mode)
700
self.put_bytes_non_atomic(relpath, b"", mode)
672
701
abspath = self._remote_path(relpath)
673
702
# TODO: jam 20060816 paramiko doesn't publicly expose a way to
674
703
# set the file mode at create time. If it does, use it.
678
707
handle = self._get_sftp().file(abspath, mode='wb')
679
708
handle.set_pipelined(True)
680
except (paramiko.SSHException, IOError), e:
709
except (paramiko.SSHException, IOError) as e:
681
710
self._translate_io_exception(e, abspath,
682
711
': unable to open')
683
712
_file_streams[self.abspath(relpath)] = handle
715
744
if (e.args[0].startswith('Directory not empty: ')
716
745
or getattr(e, 'errno', None) == errno.ENOTEMPTY):
717
746
raise errors.DirectoryNotEmpty(path, str(e))
747
if e.args == ('Operation unsupported',):
748
raise errors.TransportNotPossible()
718
749
mutter('Raising exception with args %s', e.args)
719
750
if getattr(e, 'errno', None) is not None:
720
751
mutter('Raising exception with errno %s', e.errno)
733
764
result = fout.tell()
734
765
self._pump(f, fout)
736
except (IOError, paramiko.SSHException), e:
767
except (IOError, paramiko.SSHException) as e:
737
768
self._translate_io_exception(e, relpath, ': unable to append')
739
770
def rename(self, rel_from, rel_to):
742
773
self._get_sftp().rename(self._remote_path(rel_from),
743
774
self._remote_path(rel_to))
744
except (IOError, paramiko.SSHException), e:
775
except (IOError, paramiko.SSHException) as e:
745
776
self._translate_io_exception(e, rel_from,
746
777
': unable to rename to %r' % (rel_to))
755
786
fancy_rename(abs_from, abs_to,
756
787
rename_func=sftp.rename,
757
788
unlink_func=sftp.remove)
758
except (IOError, paramiko.SSHException), e:
789
except (IOError, paramiko.SSHException) as e:
759
790
self._translate_io_exception(e, abs_from,
760
791
': unable to rename to %r' % (abs_to))
770
801
path = self._remote_path(relpath)
772
803
self._get_sftp().remove(path)
773
except (IOError, paramiko.SSHException), e:
804
except (IOError, paramiko.SSHException) as e:
774
805
self._translate_io_exception(e, path, ': unable to delete')
776
807
def external_url(self):
777
"""See bzrlib.transport.Transport.external_url."""
808
"""See breezy.transport.Transport.external_url."""
778
809
# the external path for SFTP is the base
795
826
entries = self._get_sftp().listdir(path)
796
827
self._report_activity(sum(map(len, entries)), 'read')
797
except (IOError, paramiko.SSHException), e:
828
except (IOError, paramiko.SSHException) as e:
798
829
self._translate_io_exception(e, path, ': failed to list_dir')
799
830
return [urlutils.escape(entry) for entry in entries]
803
834
path = self._remote_path(relpath)
805
836
return self._get_sftp().rmdir(path)
806
except (IOError, paramiko.SSHException), e:
837
except (IOError, paramiko.SSHException) as e:
807
838
self._translate_io_exception(e, path, ': failed to rmdir')
809
840
def stat(self, relpath):
811
842
path = self._remote_path(relpath)
813
844
return self._get_sftp().lstat(path)
814
except (IOError, paramiko.SSHException), e:
845
except (IOError, paramiko.SSHException) as e:
815
846
self._translate_io_exception(e, path, ': unable to stat')
817
848
def readlink(self, relpath):
818
849
"""See Transport.readlink."""
819
850
path = self._remote_path(relpath)
821
return self._get_sftp().readlink(path)
822
except (IOError, paramiko.SSHException), e:
852
return self._get_sftp().readlink(self._remote_path(path))
853
except (IOError, paramiko.SSHException) as e:
823
854
self._translate_io_exception(e, path, ': unable to readlink')
825
856
def symlink(self, source, link_name):
826
857
"""See Transport.symlink."""
828
859
conn = self._get_sftp()
829
sftp_retval = conn.symlink(source, link_name)
830
if SFTP_OK != sftp_retval:
831
raise TransportError(
832
'%r: unable to create symlink to %r' % (link_name, source),
835
except (IOError, paramiko.SSHException), e:
860
sftp_retval = conn.symlink(source, self._remote_path(link_name))
861
except (IOError, paramiko.SSHException) as e:
836
862
self._translate_io_exception(e, link_name,
837
863
': unable to create symlink to %r' % (source))
848
874
def unlock(self):
876
def __exit__(self, exc_type, exc_val, exc_tb):
850
880
return BogusLock(relpath)
852
882
def lock_write(self, relpath):
893
923
raise TransportError('Expected an SFTP handle')
894
924
handle = msg.get_string()
895
925
return SFTPFile(self._get_sftp(), handle, 'wb', -1)
896
except (paramiko.SSHException, IOError), e:
926
except (paramiko.SSHException, IOError) as e:
897
927
self._translate_io_exception(e, abspath, ': unable to open',
898
928
failure_exc=FileExists)
908
938
def get_test_permutations():
909
939
"""Return the permutations to be used in testing."""
910
from bzrlib.tests import stub_sftp
940
from ..tests import stub_sftp
911
941
return [(SFTPTransport, stub_sftp.SFTPAbsoluteServer),
912
942
(SFTPTransport, stub_sftp.SFTPHomeDirServer),
913
943
(SFTPTransport, stub_sftp.SFTPSiblingAbsoluteServer),