1
# Copyright (C) 2005 Robey Pointer <robey@lag.net>
2
# Copyright (C) 2005, 2006 Canonical Ltd
4
# This program is free software; you can redistribute it and/or modify
5
# it under the terms of the GNU General Public License as published by
6
# the Free Software Foundation; either version 2 of the License, or
7
# (at your option) any later version.
9
# This program is distributed in the hope that it will be useful,
10
# but WITHOUT ANY WARRANTY; without even the implied warranty of
11
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12
# GNU General Public License for more details.
14
# You should have received a copy of the GNU General Public License
15
# along with this program; if not, write to the Free Software
16
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
18
"""Implementation of Transport over SFTP, using paramiko."""
33
from bzrlib.config import config_dir, ensure_config_dir_exists
34
from bzrlib.errors import (ConnectionError,
36
TransportNotPossible, NoSuchFile, PathNotChild,
42
from bzrlib.osutils import pathjoin, fancy_rename
43
from bzrlib.trace import mutter, warning, error
44
from bzrlib.transport import (
45
register_urlparse_netloc_protocol,
55
except ImportError, e:
56
raise ParamikoNotPresent(e)
58
from paramiko.sftp import (SFTP_FLAG_WRITE, SFTP_FLAG_CREATE,
59
SFTP_FLAG_EXCL, SFTP_FLAG_TRUNC,
61
from paramiko.sftp_attr import SFTPAttributes
62
from paramiko.sftp_file import SFTPFile
63
from paramiko.sftp_client import SFTPClient
66
register_urlparse_netloc_protocol('sftp')
69
def os_specific_subprocess_params():
70
"""Get O/S specific subprocess parameters."""
71
if sys.platform == 'win32':
72
# setting the process group and closing fds is not supported on
76
# we close fds as the child process does not need them to be open.
77
# we set the process group so that signals from the keyboard like
78
# 'SIGINT' - KeyboardInterrupt - are not recieved in the child procecss
79
# if we do not do this, then the sftp/ssh subprocesses will terminate
80
# when a user hits CTRL-C, and we are unable to use them to unlock the
81
# remote branch/repository etc.
82
return {'preexec_fn': os.setpgrp,
87
# don't use prefetch unless paramiko version >= 1.5.2 (there were bugs earlier)
88
_default_do_prefetch = False
89
if getattr(paramiko, '__version_info__', (0, 0, 0)) >= (1, 5, 5):
90
_default_do_prefetch = True
94
def _get_ssh_vendor():
95
"""Find out what version of SSH is on the system."""
97
if _ssh_vendor is not None:
102
if 'BZR_SSH' in os.environ:
103
_ssh_vendor = os.environ['BZR_SSH']
104
if _ssh_vendor == 'paramiko':
109
p = subprocess.Popen(['ssh', '-V'],
110
stdin=subprocess.PIPE,
111
stdout=subprocess.PIPE,
112
stderr=subprocess.PIPE,
113
**os_specific_subprocess_params())
114
returncode = p.returncode
115
stdout, stderr = p.communicate()
119
if 'OpenSSH' in stderr:
120
mutter('ssh implementation is OpenSSH')
121
_ssh_vendor = 'openssh'
122
elif 'SSH Secure Shell' in stderr:
123
mutter('ssh implementation is SSH Corp.')
126
if _ssh_vendor != 'none':
129
# XXX: 20051123 jamesh
130
# A check for putty's plink or lsh would go here.
132
mutter('falling back to paramiko implementation')
136
class SFTPSubprocess:
137
"""A socket-like object that talks to an ssh subprocess via pipes."""
138
def __init__(self, hostname, vendor, port=None, user=None):
139
assert vendor in ['openssh', 'ssh']
140
if vendor == 'openssh':
142
'-oForwardX11=no', '-oForwardAgent=no',
143
'-oClearAllForwardings=yes', '-oProtocol=2',
144
'-oNoHostAuthenticationForLocalhost=yes']
146
args.extend(['-p', str(port)])
148
args.extend(['-l', user])
149
args.extend(['-s', hostname, 'sftp'])
150
elif vendor == 'ssh':
153
args.extend(['-p', str(port)])
155
args.extend(['-l', user])
156
args.extend(['-s', 'sftp', hostname])
158
self.proc = subprocess.Popen(args,
159
stdin=subprocess.PIPE,
160
stdout=subprocess.PIPE,
161
**os_specific_subprocess_params())
163
def send(self, data):
164
return os.write(self.proc.stdin.fileno(), data)
166
def recv_ready(self):
167
# TODO: jam 20051215 this function is necessary to support the
168
# pipelined() function. In reality, it probably should use
169
# poll() or select() to actually return if there is data
170
# available, otherwise we probably don't get any benefit
173
def recv(self, count):
174
return os.read(self.proc.stdout.fileno(), count)
177
self.proc.stdin.close()
178
self.proc.stdout.close()
182
class LoopbackSFTP(object):
183
"""Simple wrapper for a socket that pretends to be a paramiko Channel."""
185
def __init__(self, sock):
188
def send(self, data):
189
return self.__socket.send(data)
192
return self.__socket.recv(n)
194
def recv_ready(self):
198
self.__socket.close()
204
# This is a weakref dictionary, so that we can reuse connections
205
# that are still active. Long term, it might be nice to have some
206
# sort of expiration policy, such as disconnect if inactive for
207
# X seconds. But that requires a lot more fanciness.
208
_connected_hosts = weakref.WeakValueDictionary()
210
def clear_connection_cache():
211
"""Remove all hosts from the SFTP connection cache.
213
Primarily useful for test cases wanting to force garbage collection.
215
_connected_hosts.clear()
218
def load_host_keys():
220
Load system host keys (probably doesn't work on windows) and any
221
"discovered" keys from previous sessions.
223
global SYSTEM_HOSTKEYS, BZR_HOSTKEYS
225
SYSTEM_HOSTKEYS = paramiko.util.load_host_keys(os.path.expanduser('~/.ssh/known_hosts'))
227
mutter('failed to load system host keys: ' + str(e))
228
bzr_hostkey_path = pathjoin(config_dir(), 'ssh_host_keys')
230
BZR_HOSTKEYS = paramiko.util.load_host_keys(bzr_hostkey_path)
232
mutter('failed to load bzr host keys: ' + str(e))
236
def save_host_keys():
238
Save "discovered" host keys in $(config)/ssh_host_keys/.
240
global SYSTEM_HOSTKEYS, BZR_HOSTKEYS
241
bzr_hostkey_path = pathjoin(config_dir(), 'ssh_host_keys')
242
ensure_config_dir_exists()
245
f = open(bzr_hostkey_path, 'w')
246
f.write('# SSH host keys collected by bzr\n')
247
for hostname, keys in BZR_HOSTKEYS.iteritems():
248
for keytype, key in keys.iteritems():
249
f.write('%s %s %s\n' % (hostname, keytype, key.get_base64()))
252
mutter('failed to save bzr host keys: ' + str(e))
255
class SFTPLock(object):
256
"""This fakes a lock in a remote location."""
257
__slots__ = ['path', 'lock_path', 'lock_file', 'transport']
258
def __init__(self, path, transport):
259
assert isinstance(transport, SFTPTransport)
261
self.lock_file = None
263
self.lock_path = path + '.write-lock'
264
self.transport = transport
266
# RBC 20060103 FIXME should we be using private methods here ?
267
abspath = transport._remote_path(self.lock_path)
268
self.lock_file = transport._sftp_open_exclusive(abspath)
270
raise LockError('File %r already locked' % (self.path,))
273
"""Should this warn, or actually try to cleanup?"""
275
warning("SFTPLock %r not explicitly unlocked" % (self.path,))
279
if not self.lock_file:
281
self.lock_file.close()
282
self.lock_file = None
284
self.transport.delete(self.lock_path)
285
except (NoSuchFile,):
286
# What specific errors should we catch here?
289
class SFTPTransport (Transport):
291
Transport implementation for SFTP access.
293
_do_prefetch = _default_do_prefetch
295
def __init__(self, base, clone_from=None):
296
assert base.startswith('sftp://')
297
self._parse_url(base)
298
base = self._unparse_url()
301
super(SFTPTransport, self).__init__(base)
302
if clone_from is None:
305
# use the same ssh connection, etc
306
self._sftp = clone_from._sftp
307
# super saves 'self.base'
309
def should_cache(self):
311
Return True if the data pulled across should be cached locally.
315
def clone(self, offset=None):
317
Return a new SFTPTransport with root at self.base + offset.
318
We share the same SFTP session between such transports, because it's
319
fairly expensive to set them up.
322
return SFTPTransport(self.base, self)
324
return SFTPTransport(self.abspath(offset), self)
326
def abspath(self, relpath):
328
Return the full url to the given relative path.
330
@param relpath: the relative path or path components
331
@type relpath: str or list
333
return self._unparse_url(self._remote_path(relpath))
335
def _remote_path(self, relpath):
336
"""Return the path to be passed along the sftp protocol for relpath.
338
relpath is a urlencoded string.
340
# FIXME: share the common code across transports
341
assert isinstance(relpath, basestring)
342
relpath = urlunescape(relpath).split('/')
343
basepath = self._path.split('/')
344
if len(basepath) > 0 and basepath[-1] == '':
345
basepath = basepath[:-1]
349
if len(basepath) == 0:
350
# In most filesystems, a request for the parent
351
# of root, just returns root.
359
path = '/'.join(basepath)
362
def relpath(self, abspath):
363
username, password, host, port, path = self._split_url(abspath)
365
if (username != self._username):
366
error.append('username mismatch')
367
if (host != self._host):
368
error.append('host mismatch')
369
if (port != self._port):
370
error.append('port mismatch')
371
if (not path.startswith(self._path)):
372
error.append('path mismatch')
374
extra = ': ' + ', '.join(error)
375
raise PathNotChild(abspath, self.base, extra=extra)
377
return path[pl:].strip('/')
379
def has(self, relpath):
381
Does the target location exist?
384
self._sftp.stat(self._remote_path(relpath))
389
def get(self, relpath):
391
Get the file at the given relative path.
393
:param relpath: The relative path to the file
396
path = self._remote_path(relpath)
397
f = self._sftp.file(path, mode='rb')
398
if self._do_prefetch and (getattr(f, 'prefetch', None) is not None):
401
except (IOError, paramiko.SSHException), e:
402
self._translate_io_exception(e, path, ': error retrieving')
404
def get_partial(self, relpath, start, length=None):
406
Get just part of a file.
408
:param relpath: Path to the file, relative to base
409
:param start: The starting position to read from
410
:param length: The length to read. A length of None indicates
411
read to the end of the file.
412
:return: A file-like object containing at least the specified bytes.
413
Some implementations may return objects which can be read
414
past this length, but this is not guaranteed.
416
# TODO: implement get_partial_multi to help with knit support
417
f = self.get(relpath)
419
if self._do_prefetch and hasattr(f, 'prefetch'):
423
def put(self, relpath, f, mode=None):
425
Copy the file-like or string object into the location.
427
:param relpath: Location to put the contents, relative to base.
428
:param f: File-like or string object.
429
:param mode: The final mode for the file
431
final_path = self._remote_path(relpath)
432
self._put(final_path, f, mode=mode)
434
def _put(self, abspath, f, mode=None):
435
"""Helper function so both put() and copy_abspaths can reuse the code"""
436
tmp_abspath = '%s.tmp.%.9f.%d.%d' % (abspath, time.time(),
437
os.getpid(), random.randint(0,0x7FFFFFFF))
438
fout = self._sftp_open_exclusive(tmp_abspath, mode=mode)
442
fout.set_pipelined(True)
444
except (IOError, paramiko.SSHException), e:
445
self._translate_io_exception(e, tmp_abspath)
447
self._sftp.chmod(tmp_abspath, mode)
450
self._rename_and_overwrite(tmp_abspath, abspath)
452
# If we fail, try to clean up the temporary file
453
# before we throw the exception
454
# but don't let another exception mess things up
455
# Write out the traceback, because otherwise
456
# the catch and throw destroys it
458
mutter(traceback.format_exc())
462
self._sftp.remove(tmp_abspath)
464
# raise the saved except
466
# raise the original with its traceback if we can.
469
def iter_files_recursive(self):
470
"""Walk the relative paths of all files in this transport."""
471
queue = list(self.list_dir('.'))
473
relpath = urllib.quote(queue.pop(0))
474
st = self.stat(relpath)
475
if stat.S_ISDIR(st.st_mode):
476
for i, basename in enumerate(self.list_dir(relpath)):
477
queue.insert(i, relpath+'/'+basename)
481
def mkdir(self, relpath, mode=None):
482
"""Create a directory at the given path."""
484
path = self._remote_path(relpath)
485
# In the paramiko documentation, it says that passing a mode flag
486
# will filtered against the server umask.
487
# StubSFTPServer does not do this, which would be nice, because it is
488
# what we really want :)
489
# However, real servers do use umask, so we really should do it that way
490
self._sftp.mkdir(path)
492
self._sftp.chmod(path, mode=mode)
493
except (paramiko.SSHException, IOError), e:
494
self._translate_io_exception(e, path, ': unable to mkdir',
495
failure_exc=FileExists)
497
def _translate_io_exception(self, e, path, more_info='',
498
failure_exc=PathError):
499
"""Translate a paramiko or IOError into a friendlier exception.
501
:param e: The original exception
502
:param path: The path in question when the error is raised
503
:param more_info: Extra information that can be included,
504
such as what was going on
505
:param failure_exc: Paramiko has the super fun ability to raise completely
506
opaque errors that just set "e.args = ('Failure',)" with
508
If this parameter is set, it defines the exception
509
to raise in these cases.
511
# paramiko seems to generate detailless errors.
512
self._translate_error(e, path, raise_generic=False)
513
if hasattr(e, 'args'):
514
if (e.args == ('No such file or directory',) or
515
e.args == ('No such file',)):
516
raise NoSuchFile(path, str(e) + more_info)
517
if (e.args == ('mkdir failed',)):
518
raise FileExists(path, str(e) + more_info)
519
# strange but true, for the paramiko server.
520
if (e.args == ('Failure',)):
521
raise failure_exc(path, str(e) + more_info)
522
mutter('Raising exception with args %s', e.args)
523
if hasattr(e, 'errno'):
524
mutter('Raising exception with errno %s', e.errno)
527
def append(self, relpath, f, mode=None):
529
Append the text in the file-like object into the final
533
path = self._remote_path(relpath)
534
fout = self._sftp.file(path, 'ab')
536
self._sftp.chmod(path, mode)
540
except (IOError, paramiko.SSHException), e:
541
self._translate_io_exception(e, relpath, ': unable to append')
543
def rename(self, rel_from, rel_to):
544
"""Rename without special overwriting"""
546
self._sftp.rename(self._remote_path(rel_from),
547
self._remote_path(rel_to))
548
except (IOError, paramiko.SSHException), e:
549
self._translate_io_exception(e, rel_from,
550
': unable to rename to %r' % (rel_to))
552
def _rename_and_overwrite(self, abs_from, abs_to):
553
"""Do a fancy rename on the remote server.
555
Using the implementation provided by osutils.
558
fancy_rename(abs_from, abs_to,
559
rename_func=self._sftp.rename,
560
unlink_func=self._sftp.remove)
561
except (IOError, paramiko.SSHException), e:
562
self._translate_io_exception(e, abs_from, ': unable to rename to %r' % (abs_to))
564
def move(self, rel_from, rel_to):
565
"""Move the item at rel_from to the location at rel_to"""
566
path_from = self._remote_path(rel_from)
567
path_to = self._remote_path(rel_to)
568
self._rename_and_overwrite(path_from, path_to)
570
def delete(self, relpath):
571
"""Delete the item at relpath"""
572
path = self._remote_path(relpath)
574
self._sftp.remove(path)
575
except (IOError, paramiko.SSHException), e:
576
self._translate_io_exception(e, path, ': unable to delete')
579
"""Return True if this store supports listing."""
582
def list_dir(self, relpath):
584
Return a list of all files at the given location.
586
# does anything actually use this?
587
path = self._remote_path(relpath)
589
return self._sftp.listdir(path)
590
except (IOError, paramiko.SSHException), e:
591
self._translate_io_exception(e, path, ': failed to list_dir')
593
def rmdir(self, relpath):
594
"""See Transport.rmdir."""
595
path = self._remote_path(relpath)
597
return self._sftp.rmdir(path)
598
except (IOError, paramiko.SSHException), e:
599
self._translate_io_exception(e, path, ': failed to rmdir')
601
def stat(self, relpath):
602
"""Return the stat information for a file."""
603
path = self._remote_path(relpath)
605
return self._sftp.stat(path)
606
except (IOError, paramiko.SSHException), e:
607
self._translate_io_exception(e, path, ': unable to stat')
609
def lock_read(self, relpath):
611
Lock the given file for shared (read) access.
612
:return: A lock object, which has an unlock() member function
614
# FIXME: there should be something clever i can do here...
615
class BogusLock(object):
616
def __init__(self, path):
620
return BogusLock(relpath)
622
def lock_write(self, relpath):
624
Lock the given file for exclusive (write) access.
625
WARNING: many transports do not support this, so trying avoid using it
627
:return: A lock object, which has an unlock() member function
629
# This is a little bit bogus, but basically, we create a file
630
# which should not already exist, and if it does, we assume
631
# that there is a lock, and if it doesn't, the we assume
632
# that we have taken the lock.
633
return SFTPLock(relpath, self)
635
def _unparse_url(self, path=None):
638
path = urllib.quote(path)
639
# handle homedir paths
640
if not path.startswith('/'):
642
netloc = urllib.quote(self._host)
643
if self._username is not None:
644
netloc = '%s@%s' % (urllib.quote(self._username), netloc)
645
if self._port is not None:
646
netloc = '%s:%d' % (netloc, self._port)
647
return urlparse.urlunparse(('sftp', netloc, path, '', '', ''))
649
def _split_url(self, url):
650
if isinstance(url, unicode):
651
# TODO: Disallow unicode urls
652
#raise InvalidURL(url, 'urls must not be unicode.')
653
url = url.encode('ascii')
654
(scheme, netloc, path, params,
655
query, fragment) = urlparse.urlparse(url, allow_fragments=False)
656
assert scheme == 'sftp'
657
username = password = host = port = None
659
username, host = netloc.split('@', 1)
661
username, password = username.split(':', 1)
662
password = urllib.unquote(password)
663
username = urllib.unquote(username)
668
host, port = host.rsplit(':', 1)
672
# TODO: Should this be ConnectionError?
673
raise TransportError('%s: invalid port number' % port)
674
host = urllib.unquote(host)
676
path = urlunescape(path)
678
# the initial slash should be removed from the path, and treated
679
# as a homedir relative path (the path begins with a double slash
680
# if it is absolute).
681
# see draft-ietf-secsh-scp-sftp-ssh-uri-03.txt
682
# RBC 20060118 we are not using this as its too user hostile. instead
683
# we are following lftp and using /~/foo to mean '~/foo'.
684
# handle homedir paths
685
if path.startswith('/~/'):
689
return (username, password, host, port, path)
691
def _parse_url(self, url):
692
(self._username, self._password,
693
self._host, self._port, self._path) = self._split_url(url)
695
def _sftp_connect(self):
696
"""Connect to the remote sftp server.
697
After this, self._sftp should have a valid connection (or
698
we raise an TransportError 'could not connect').
700
TODO: Raise a more reasonable ConnectionFailed exception
702
global _connected_hosts
704
idx = (self._host, self._port, self._username)
706
self._sftp = _connected_hosts[idx]
711
vendor = _get_ssh_vendor()
712
if vendor == 'loopback':
713
sock = socket.socket()
714
sock.connect((self._host, self._port))
715
self._sftp = SFTPClient(LoopbackSFTP(sock))
716
elif vendor != 'none':
717
sock = SFTPSubprocess(self._host, vendor, self._port,
719
self._sftp = SFTPClient(sock)
721
self._paramiko_connect()
723
_connected_hosts[idx] = self._sftp
725
def _paramiko_connect(self):
726
global SYSTEM_HOSTKEYS, BZR_HOSTKEYS
731
t = paramiko.Transport((self._host, self._port or 22))
732
t.set_log_channel('bzr.paramiko')
734
except paramiko.SSHException, e:
735
raise ConnectionError('Unable to reach SSH host %s:%d' %
736
(self._host, self._port), e)
738
server_key = t.get_remote_server_key()
739
server_key_hex = paramiko.util.hexify(server_key.get_fingerprint())
740
keytype = server_key.get_name()
741
if SYSTEM_HOSTKEYS.has_key(self._host) and SYSTEM_HOSTKEYS[self._host].has_key(keytype):
742
our_server_key = SYSTEM_HOSTKEYS[self._host][keytype]
743
our_server_key_hex = paramiko.util.hexify(our_server_key.get_fingerprint())
744
elif BZR_HOSTKEYS.has_key(self._host) and BZR_HOSTKEYS[self._host].has_key(keytype):
745
our_server_key = BZR_HOSTKEYS[self._host][keytype]
746
our_server_key_hex = paramiko.util.hexify(our_server_key.get_fingerprint())
748
warning('Adding %s host key for %s: %s' % (keytype, self._host, server_key_hex))
749
if not BZR_HOSTKEYS.has_key(self._host):
750
BZR_HOSTKEYS[self._host] = {}
751
BZR_HOSTKEYS[self._host][keytype] = server_key
752
our_server_key = server_key
753
our_server_key_hex = paramiko.util.hexify(our_server_key.get_fingerprint())
755
if server_key != our_server_key:
756
filename1 = os.path.expanduser('~/.ssh/known_hosts')
757
filename2 = pathjoin(config_dir(), 'ssh_host_keys')
758
raise TransportError('Host keys for %s do not match! %s != %s' % \
759
(self._host, our_server_key_hex, server_key_hex),
760
['Try editing %s or %s' % (filename1, filename2)])
765
self._sftp = t.open_sftp_client()
766
except paramiko.SSHException, e:
767
raise ConnectionError('Unable to start sftp client %s:%d' %
768
(self._host, self._port), e)
770
def _sftp_auth(self, transport):
771
# paramiko requires a username, but it might be none if nothing was supplied
772
# use the local username, just in case.
773
# We don't override self._username, because if we aren't using paramiko,
774
# the username might be specified in ~/.ssh/config and we don't want to
775
# force it to something else
776
# Also, it would mess up the self.relpath() functionality
777
username = self._username or getpass.getuser()
779
# Paramiko tries to open a socket.AF_UNIX in order to connect
780
# to ssh-agent. That attribute doesn't exist on win32 (it does in cygwin)
781
# so we get an AttributeError exception. For now, just don't try to
782
# connect to an agent if we are on win32
783
if sys.platform != 'win32':
784
agent = paramiko.Agent()
785
for key in agent.get_keys():
786
mutter('Trying SSH agent key %s' % paramiko.util.hexify(key.get_fingerprint()))
788
transport.auth_publickey(username, key)
790
except paramiko.SSHException, e:
793
# okay, try finding id_rsa or id_dss? (posix only)
794
if self._try_pkey_auth(transport, paramiko.RSAKey, username, 'id_rsa'):
796
if self._try_pkey_auth(transport, paramiko.DSSKey, username, 'id_dsa'):
801
transport.auth_password(username, self._password)
803
except paramiko.SSHException, e:
806
# FIXME: Don't keep a password held in memory if you can help it
807
#self._password = None
809
# give up and ask for a password
810
password = bzrlib.ui.ui_factory.get_password(
811
prompt='SSH %(user)s@%(host)s password',
812
user=username, host=self._host)
814
transport.auth_password(username, password)
815
except paramiko.SSHException, e:
816
raise ConnectionError('Unable to authenticate to SSH host as %s@%s' %
817
(username, self._host), e)
819
def _try_pkey_auth(self, transport, pkey_class, username, filename):
820
filename = os.path.expanduser('~/.ssh/' + filename)
822
key = pkey_class.from_private_key_file(filename)
823
transport.auth_publickey(username, key)
825
except paramiko.PasswordRequiredException:
826
password = bzrlib.ui.ui_factory.get_password(
827
prompt='SSH %(filename)s password',
830
key = pkey_class.from_private_key_file(filename, password)
831
transport.auth_publickey(username, key)
833
except paramiko.SSHException:
834
mutter('SSH authentication via %s key failed.' % (os.path.basename(filename),))
835
except paramiko.SSHException:
836
mutter('SSH authentication via %s key failed.' % (os.path.basename(filename),))
841
def _sftp_open_exclusive(self, abspath, mode=None):
842
"""Open a remote path exclusively.
844
SFTP supports O_EXCL (SFTP_FLAG_EXCL), which fails if
845
the file already exists. However it does not expose this
846
at the higher level of SFTPClient.open(), so we have to
849
WARNING: This breaks the SFTPClient abstraction, so it
850
could easily break against an updated version of paramiko.
852
:param abspath: The remote absolute path where the file should be opened
853
:param mode: The mode permissions bits for the new file
855
path = self._sftp._adjust_cwd(abspath)
856
attr = SFTPAttributes()
859
omode = (SFTP_FLAG_WRITE | SFTP_FLAG_CREATE
860
| SFTP_FLAG_TRUNC | SFTP_FLAG_EXCL)
862
t, msg = self._sftp._request(CMD_OPEN, path, omode, attr)
864
raise TransportError('Expected an SFTP handle')
865
handle = msg.get_string()
866
return SFTPFile(self._sftp, handle, 'wb', -1)
867
except (paramiko.SSHException, IOError), e:
868
self._translate_io_exception(e, abspath, ': unable to open',
869
failure_exc=FileExists)
872
# ------------- server test implementation --------------
876
from bzrlib.tests.stub_sftp import StubServer, StubSFTPServer
878
STUB_SERVER_KEY = """
879
-----BEGIN RSA PRIVATE KEY-----
880
MIICWgIBAAKBgQDTj1bqB4WmayWNPB+8jVSYpZYk80Ujvj680pOTh2bORBjbIAyz
881
oWGW+GUjzKxTiiPvVmxFgx5wdsFvF03v34lEVVhMpouqPAYQ15N37K/ir5XY+9m/
882
d8ufMCkjeXsQkKqFbAlQcnWMCRnOoPHS3I4vi6hmnDDeeYTSRvfLbW0fhwIBIwKB
883
gBIiOqZYaoqbeD9OS9z2K9KR2atlTxGxOJPXiP4ESqP3NVScWNwyZ3NXHpyrJLa0
884
EbVtzsQhLn6rF+TzXnOlcipFvjsem3iYzCpuChfGQ6SovTcOjHV9z+hnpXvQ/fon
885
soVRZY65wKnF7IAoUwTmJS9opqgrN6kRgCd3DASAMd1bAkEA96SBVWFt/fJBNJ9H
886
tYnBKZGw0VeHOYmVYbvMSstssn8un+pQpUm9vlG/bp7Oxd/m+b9KWEh2xPfv6zqU
887
avNwHwJBANqzGZa/EpzF4J8pGti7oIAPUIDGMtfIcmqNXVMckrmzQ2vTfqtkEZsA
888
4rE1IERRyiJQx6EJsz21wJmGV9WJQ5kCQQDwkS0uXqVdFzgHO6S++tjmjYcxwr3g
889
H0CoFYSgbddOT6miqRskOQF3DZVkJT3kyuBgU2zKygz52ukQZMqxCb1fAkASvuTv
890
qfpH87Qq5kQhNKdbbwbmd2NxlNabazPijWuphGTdW0VfJdWfklyS2Kr+iqrs/5wV
891
HhathJt636Eg7oIjAkA8ht3MQ+XSl9yIJIS8gVpbPxSw5OMfw0PjVE7tBdQruiSc
892
nvuQES5C9BMHjF39LZiGH1iLQy7FgdHyoP+eodI7
893
-----END RSA PRIVATE KEY-----
897
class SingleListener(threading.Thread):
899
def __init__(self, callback):
900
threading.Thread.__init__(self)
901
self._callback = callback
902
self._socket = socket.socket()
903
self._socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
904
self._socket.bind(('localhost', 0))
905
self._socket.listen(1)
906
self.port = self._socket.getsockname()[1]
907
self.stop_event = threading.Event()
910
s, _ = self._socket.accept()
911
# now close the listen socket
914
self._callback(s, self.stop_event)
916
pass #Ignore socket errors
918
# probably a failed test
919
warning('Exception from within unit test server thread: %r' % x)
922
self.stop_event.set()
923
# use a timeout here, because if the test fails, the server thread may
924
# never notice the stop_event.
928
class SFTPServer(Server):
929
"""Common code for SFTP server facilities."""
932
self._original_vendor = None
934
self._server_homedir = None
935
self._listener = None
937
self._vendor = 'none'
941
def _get_sftp_url(self, path):
942
"""Calculate an sftp url to this server for path."""
943
return 'sftp://foo:bar@localhost:%d/%s' % (self._listener.port, path)
945
def log(self, message):
946
"""StubServer uses this to log when a new server is created."""
947
self.logs.append(message)
949
def _run_server(self, s, stop_event):
950
ssh_server = paramiko.Transport(s)
951
key_file = os.path.join(self._homedir, 'test_rsa.key')
952
file(key_file, 'w').write(STUB_SERVER_KEY)
953
host_key = paramiko.RSAKey.from_private_key_file(key_file)
954
ssh_server.add_server_key(host_key)
955
server = StubServer(self)
956
ssh_server.set_subsystem_handler('sftp', paramiko.SFTPServer,
957
StubSFTPServer, root=self._root,
958
home=self._server_homedir)
959
event = threading.Event()
960
ssh_server.start_server(event, server)
962
stop_event.wait(30.0)
966
self._original_vendor = _ssh_vendor
967
_ssh_vendor = self._vendor
968
self._homedir = os.getcwdu()
969
if self._server_homedir is None:
970
self._server_homedir = self._homedir
972
# FIXME WINDOWS: _root should be _server_homedir[0]:/
973
self._listener = SingleListener(self._run_server)
974
self._listener.setDaemon(True)
975
self._listener.start()
978
"""See bzrlib.transport.Server.tearDown."""
980
self._listener.stop()
981
_ssh_vendor = self._original_vendor
984
class SFTPFullAbsoluteServer(SFTPServer):
985
"""A test server for sftp transports, using absolute urls and ssh."""
988
"""See bzrlib.transport.Server.get_url."""
989
return self._get_sftp_url(urlescape(self._homedir[1:]))
992
class SFTPServerWithoutSSH(SFTPServer):
993
"""An SFTP server that uses a simple TCP socket pair rather than SSH."""
996
super(SFTPServerWithoutSSH, self).__init__()
997
self._vendor = 'loopback'
999
def _run_server(self, sock, stop_event):
1000
class FakeChannel(object):
1001
def get_transport(self):
1003
def get_log_channel(self):
1007
def get_hexdump(self):
1010
server = paramiko.SFTPServer(FakeChannel(), 'sftp', StubServer(self), StubSFTPServer,
1011
root=self._root, home=self._server_homedir)
1012
server.start_subsystem('sftp', None, sock)
1013
server.finish_subsystem()
1016
class SFTPAbsoluteServer(SFTPServerWithoutSSH):
1017
"""A test server for sftp transports, using absolute urls."""
1020
"""See bzrlib.transport.Server.get_url."""
1021
return self._get_sftp_url(urlescape(self._homedir[1:]))
1024
class SFTPHomeDirServer(SFTPServerWithoutSSH):
1025
"""A test server for sftp transports, using homedir relative urls."""
1028
"""See bzrlib.transport.Server.get_url."""
1029
return self._get_sftp_url("~/")
1032
class SFTPSiblingAbsoluteServer(SFTPAbsoluteServer):
1033
"""A test servere for sftp transports, using absolute urls to non-home."""
1036
self._server_homedir = '/dev/noone/runs/tests/here'
1037
super(SFTPSiblingAbsoluteServer, self).setUp()
1040
def get_test_permutations():
1041
"""Return the permutations to be used in testing."""
1042
return [(SFTPTransport, SFTPAbsoluteServer),
1043
(SFTPTransport, SFTPHomeDirServer),
1044
(SFTPTransport, SFTPSiblingAbsoluteServer),