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,
54
except ImportError, e:
55
raise ParamikoNotPresent(e)
57
from paramiko.sftp import (SFTP_FLAG_WRITE, SFTP_FLAG_CREATE,
58
SFTP_FLAG_EXCL, SFTP_FLAG_TRUNC,
60
from paramiko.sftp_attr import SFTPAttributes
61
from paramiko.sftp_file import SFTPFile
62
from paramiko.sftp_client import SFTPClient
65
register_urlparse_netloc_protocol('sftp')
68
def os_specific_subprocess_params():
69
"""Get O/S specific subprocess parameters."""
70
if sys.platform == 'win32':
71
# setting the process group and closing fds is not supported on
75
# we close fds as the child process does not need them to be open.
76
# we set the process group so that signals from the keyboard like
77
# 'SIGINT' - KeyboardInterrupt - are not recieved in the child procecss
78
# if we do not do this, then the sftp/ssh subprocesses will terminate
79
# when a user hits CTRL-C, and we are unable to use them to unlock the
80
# remote branch/repository etc.
81
return {'preexec_fn': os.setpgrp,
86
# don't use prefetch unless paramiko version >= 1.5.2 (there were bugs earlier)
87
_default_do_prefetch = False
88
if getattr(paramiko, '__version_info__', (0, 0, 0)) >= (1, 5, 5):
89
_default_do_prefetch = True
93
def _get_ssh_vendor():
94
"""Find out what version of SSH is on the system."""
96
if _ssh_vendor is not None:
101
if 'BZR_SSH' in os.environ:
102
_ssh_vendor = os.environ['BZR_SSH']
103
if _ssh_vendor == 'paramiko':
108
p = subprocess.Popen(['ssh', '-V'],
109
stdin=subprocess.PIPE,
110
stdout=subprocess.PIPE,
111
stderr=subprocess.PIPE,
112
**os_specific_subprocess_params())
113
returncode = p.returncode
114
stdout, stderr = p.communicate()
118
if 'OpenSSH' in stderr:
119
mutter('ssh implementation is OpenSSH')
120
_ssh_vendor = 'openssh'
121
elif 'SSH Secure Shell' in stderr:
122
mutter('ssh implementation is SSH Corp.')
125
if _ssh_vendor != 'none':
128
# XXX: 20051123 jamesh
129
# A check for putty's plink or lsh would go here.
131
mutter('falling back to paramiko implementation')
135
class SFTPSubprocess:
136
"""A socket-like object that talks to an ssh subprocess via pipes."""
137
def __init__(self, hostname, vendor, port=None, user=None):
138
assert vendor in ['openssh', 'ssh']
139
if vendor == 'openssh':
141
'-oForwardX11=no', '-oForwardAgent=no',
142
'-oClearAllForwardings=yes', '-oProtocol=2',
143
'-oNoHostAuthenticationForLocalhost=yes']
145
args.extend(['-p', str(port)])
147
args.extend(['-l', user])
148
args.extend(['-s', hostname, 'sftp'])
149
elif vendor == 'ssh':
152
args.extend(['-p', str(port)])
154
args.extend(['-l', user])
155
args.extend(['-s', 'sftp', hostname])
157
self.proc = subprocess.Popen(args,
158
stdin=subprocess.PIPE,
159
stdout=subprocess.PIPE,
160
**os_specific_subprocess_params())
162
def send(self, data):
163
return os.write(self.proc.stdin.fileno(), data)
165
def recv_ready(self):
166
# TODO: jam 20051215 this function is necessary to support the
167
# pipelined() function. In reality, it probably should use
168
# poll() or select() to actually return if there is data
169
# available, otherwise we probably don't get any benefit
172
def recv(self, count):
173
return os.read(self.proc.stdout.fileno(), count)
176
self.proc.stdin.close()
177
self.proc.stdout.close()
181
class LoopbackSFTP(object):
182
"""Simple wrapper for a socket that pretends to be a paramiko Channel."""
184
def __init__(self, sock):
187
def send(self, data):
188
return self.__socket.send(data)
191
return self.__socket.recv(n)
193
def recv_ready(self):
197
self.__socket.close()
203
# This is a weakref dictionary, so that we can reuse connections
204
# that are still active. Long term, it might be nice to have some
205
# sort of expiration policy, such as disconnect if inactive for
206
# X seconds. But that requires a lot more fanciness.
207
_connected_hosts = weakref.WeakValueDictionary()
209
def clear_connection_cache():
210
"""Remove all hosts from the SFTP connection cache.
212
Primarily useful for test cases wanting to force garbage collection.
214
_connected_hosts.clear()
217
def load_host_keys():
219
Load system host keys (probably doesn't work on windows) and any
220
"discovered" keys from previous sessions.
222
global SYSTEM_HOSTKEYS, BZR_HOSTKEYS
224
SYSTEM_HOSTKEYS = paramiko.util.load_host_keys(os.path.expanduser('~/.ssh/known_hosts'))
226
mutter('failed to load system host keys: ' + str(e))
227
bzr_hostkey_path = pathjoin(config_dir(), 'ssh_host_keys')
229
BZR_HOSTKEYS = paramiko.util.load_host_keys(bzr_hostkey_path)
231
mutter('failed to load bzr host keys: ' + str(e))
235
def save_host_keys():
237
Save "discovered" host keys in $(config)/ssh_host_keys/.
239
global SYSTEM_HOSTKEYS, BZR_HOSTKEYS
240
bzr_hostkey_path = pathjoin(config_dir(), 'ssh_host_keys')
241
ensure_config_dir_exists()
244
f = open(bzr_hostkey_path, 'w')
245
f.write('# SSH host keys collected by bzr\n')
246
for hostname, keys in BZR_HOSTKEYS.iteritems():
247
for keytype, key in keys.iteritems():
248
f.write('%s %s %s\n' % (hostname, keytype, key.get_base64()))
251
mutter('failed to save bzr host keys: ' + str(e))
254
class SFTPLock(object):
255
"""This fakes a lock in a remote location."""
256
__slots__ = ['path', 'lock_path', 'lock_file', 'transport']
257
def __init__(self, path, transport):
258
assert isinstance(transport, SFTPTransport)
260
self.lock_file = None
262
self.lock_path = path + '.write-lock'
263
self.transport = transport
265
# RBC 20060103 FIXME should we be using private methods here ?
266
abspath = transport._remote_path(self.lock_path)
267
self.lock_file = transport._sftp_open_exclusive(abspath)
269
raise LockError('File %r already locked' % (self.path,))
272
"""Should this warn, or actually try to cleanup?"""
274
warning("SFTPLock %r not explicitly unlocked" % (self.path,))
278
if not self.lock_file:
280
self.lock_file.close()
281
self.lock_file = None
283
self.transport.delete(self.lock_path)
284
except (NoSuchFile,):
285
# What specific errors should we catch here?
288
class SFTPTransport (Transport):
290
Transport implementation for SFTP access.
292
_do_prefetch = _default_do_prefetch
294
def __init__(self, base, clone_from=None):
295
assert base.startswith('sftp://')
296
self._parse_url(base)
297
base = self._unparse_url()
300
super(SFTPTransport, self).__init__(base)
301
if clone_from is None:
304
# use the same ssh connection, etc
305
self._sftp = clone_from._sftp
306
# super saves 'self.base'
308
def should_cache(self):
310
Return True if the data pulled across should be cached locally.
314
def clone(self, offset=None):
316
Return a new SFTPTransport with root at self.base + offset.
317
We share the same SFTP session between such transports, because it's
318
fairly expensive to set them up.
321
return SFTPTransport(self.base, self)
323
return SFTPTransport(self.abspath(offset), self)
325
def abspath(self, relpath):
327
Return the full url to the given relative path.
329
@param relpath: the relative path or path components
330
@type relpath: str or list
332
return self._unparse_url(self._remote_path(relpath))
334
def _remote_path(self, relpath):
335
"""Return the path to be passed along the sftp protocol for relpath.
337
relpath is a urlencoded string.
339
# FIXME: share the common code across transports
340
assert isinstance(relpath, basestring)
341
relpath = urllib.unquote(relpath).split('/')
342
basepath = self._path.split('/')
343
if len(basepath) > 0 and basepath[-1] == '':
344
basepath = basepath[:-1]
348
if len(basepath) == 0:
349
# In most filesystems, a request for the parent
350
# of root, just returns root.
358
path = '/'.join(basepath)
361
def relpath(self, abspath):
362
username, password, host, port, path = self._split_url(abspath)
364
if (username != self._username):
365
error.append('username mismatch')
366
if (host != self._host):
367
error.append('host mismatch')
368
if (port != self._port):
369
error.append('port mismatch')
370
if (not path.startswith(self._path)):
371
error.append('path mismatch')
373
extra = ': ' + ', '.join(error)
374
raise PathNotChild(abspath, self.base, extra=extra)
376
return path[pl:].strip('/')
378
def has(self, relpath):
380
Does the target location exist?
383
self._sftp.stat(self._remote_path(relpath))
388
def get(self, relpath):
390
Get the file at the given relative path.
392
:param relpath: The relative path to the file
395
path = self._remote_path(relpath)
396
f = self._sftp.file(path, mode='rb')
397
if self._do_prefetch and (getattr(f, 'prefetch', None) is not None):
400
except (IOError, paramiko.SSHException), e:
401
self._translate_io_exception(e, path, ': error retrieving')
403
def get_partial(self, relpath, start, length=None):
405
Get just part of a file.
407
:param relpath: Path to the file, relative to base
408
:param start: The starting position to read from
409
:param length: The length to read. A length of None indicates
410
read to the end of the file.
411
:return: A file-like object containing at least the specified bytes.
412
Some implementations may return objects which can be read
413
past this length, but this is not guaranteed.
415
# TODO: implement get_partial_multi to help with knit support
416
f = self.get(relpath)
418
if self._do_prefetch and hasattr(f, 'prefetch'):
422
def put(self, relpath, f, mode=None):
424
Copy the file-like or string object into the location.
426
:param relpath: Location to put the contents, relative to base.
427
:param f: File-like or string object.
428
:param mode: The final mode for the file
430
final_path = self._remote_path(relpath)
431
self._put(final_path, f, mode=mode)
433
def _put(self, abspath, f, mode=None):
434
"""Helper function so both put() and copy_abspaths can reuse the code"""
435
tmp_abspath = '%s.tmp.%.9f.%d.%d' % (abspath, time.time(),
436
os.getpid(), random.randint(0,0x7FFFFFFF))
437
fout = self._sftp_open_exclusive(tmp_abspath, mode=mode)
441
fout.set_pipelined(True)
443
except (IOError, paramiko.SSHException), e:
444
self._translate_io_exception(e, tmp_abspath)
446
self._sftp.chmod(tmp_abspath, mode)
449
self._rename_and_overwrite(tmp_abspath, abspath)
451
# If we fail, try to clean up the temporary file
452
# before we throw the exception
453
# but don't let another exception mess things up
454
# Write out the traceback, because otherwise
455
# the catch and throw destroys it
457
mutter(traceback.format_exc())
461
self._sftp.remove(tmp_abspath)
463
# raise the saved except
465
# raise the original with its traceback if we can.
468
def iter_files_recursive(self):
469
"""Walk the relative paths of all files in this transport."""
470
queue = list(self.list_dir('.'))
472
relpath = urllib.quote(queue.pop(0))
473
st = self.stat(relpath)
474
if stat.S_ISDIR(st.st_mode):
475
for i, basename in enumerate(self.list_dir(relpath)):
476
queue.insert(i, relpath+'/'+basename)
480
def mkdir(self, relpath, mode=None):
481
"""Create a directory at the given path."""
483
path = self._remote_path(relpath)
484
# In the paramiko documentation, it says that passing a mode flag
485
# will filtered against the server umask.
486
# StubSFTPServer does not do this, which would be nice, because it is
487
# what we really want :)
488
# However, real servers do use umask, so we really should do it that way
489
self._sftp.mkdir(path)
491
self._sftp.chmod(path, mode=mode)
492
except (paramiko.SSHException, IOError), e:
493
self._translate_io_exception(e, path, ': unable to mkdir',
494
failure_exc=FileExists)
496
def _translate_io_exception(self, e, path, more_info='',
497
failure_exc=PathError):
498
"""Translate a paramiko or IOError into a friendlier exception.
500
:param e: The original exception
501
:param path: The path in question when the error is raised
502
:param more_info: Extra information that can be included,
503
such as what was going on
504
:param failure_exc: Paramiko has the super fun ability to raise completely
505
opaque errors that just set "e.args = ('Failure',)" with
507
If this parameter is set, it defines the exception
508
to raise in these cases.
510
# paramiko seems to generate detailless errors.
511
self._translate_error(e, path, raise_generic=False)
512
if hasattr(e, 'args'):
513
if (e.args == ('No such file or directory',) or
514
e.args == ('No such file',)):
515
raise NoSuchFile(path, str(e) + more_info)
516
if (e.args == ('mkdir failed',)):
517
raise FileExists(path, str(e) + more_info)
518
# strange but true, for the paramiko server.
519
if (e.args == ('Failure',)):
520
raise failure_exc(path, str(e) + more_info)
521
mutter('Raising exception with args %s', e.args)
522
if hasattr(e, 'errno'):
523
mutter('Raising exception with errno %s', e.errno)
526
def append(self, relpath, f, mode=None):
528
Append the text in the file-like object into the final
532
path = self._remote_path(relpath)
533
fout = self._sftp.file(path, 'ab')
535
self._sftp.chmod(path, mode)
539
except (IOError, paramiko.SSHException), e:
540
self._translate_io_exception(e, relpath, ': unable to append')
542
def rename(self, rel_from, rel_to):
543
"""Rename without special overwriting"""
545
self._sftp.rename(self._remote_path(rel_from),
546
self._remote_path(rel_to))
547
except (IOError, paramiko.SSHException), e:
548
self._translate_io_exception(e, rel_from,
549
': unable to rename to %r' % (rel_to))
551
def _rename_and_overwrite(self, abs_from, abs_to):
552
"""Do a fancy rename on the remote server.
554
Using the implementation provided by osutils.
557
fancy_rename(abs_from, abs_to,
558
rename_func=self._sftp.rename,
559
unlink_func=self._sftp.remove)
560
except (IOError, paramiko.SSHException), e:
561
self._translate_io_exception(e, abs_from, ': unable to rename to %r' % (abs_to))
563
def move(self, rel_from, rel_to):
564
"""Move the item at rel_from to the location at rel_to"""
565
path_from = self._remote_path(rel_from)
566
path_to = self._remote_path(rel_to)
567
self._rename_and_overwrite(path_from, path_to)
569
def delete(self, relpath):
570
"""Delete the item at relpath"""
571
path = self._remote_path(relpath)
573
self._sftp.remove(path)
574
except (IOError, paramiko.SSHException), e:
575
self._translate_io_exception(e, path, ': unable to delete')
578
"""Return True if this store supports listing."""
581
def list_dir(self, relpath):
583
Return a list of all files at the given location.
585
# does anything actually use this?
586
path = self._remote_path(relpath)
588
return self._sftp.listdir(path)
589
except (IOError, paramiko.SSHException), e:
590
self._translate_io_exception(e, path, ': failed to list_dir')
592
def rmdir(self, relpath):
593
"""See Transport.rmdir."""
594
path = self._remote_path(relpath)
596
return self._sftp.rmdir(path)
597
except (IOError, paramiko.SSHException), e:
598
self._translate_io_exception(e, path, ': failed to rmdir')
600
def stat(self, relpath):
601
"""Return the stat information for a file."""
602
path = self._remote_path(relpath)
604
return self._sftp.stat(path)
605
except (IOError, paramiko.SSHException), e:
606
self._translate_io_exception(e, path, ': unable to stat')
608
def lock_read(self, relpath):
610
Lock the given file for shared (read) access.
611
:return: A lock object, which has an unlock() member function
613
# FIXME: there should be something clever i can do here...
614
class BogusLock(object):
615
def __init__(self, path):
619
return BogusLock(relpath)
621
def lock_write(self, relpath):
623
Lock the given file for exclusive (write) access.
624
WARNING: many transports do not support this, so trying avoid using it
626
:return: A lock object, which has an unlock() member function
628
# This is a little bit bogus, but basically, we create a file
629
# which should not already exist, and if it does, we assume
630
# that there is a lock, and if it doesn't, the we assume
631
# that we have taken the lock.
632
return SFTPLock(relpath, self)
634
def _unparse_url(self, path=None):
637
path = urllib.quote(path)
638
# handle homedir paths
639
if not path.startswith('/'):
641
netloc = urllib.quote(self._host)
642
if self._username is not None:
643
netloc = '%s@%s' % (urllib.quote(self._username), netloc)
644
if self._port is not None:
645
netloc = '%s:%d' % (netloc, self._port)
646
return urlparse.urlunparse(('sftp', netloc, path, '', '', ''))
648
def _split_url(self, url):
649
if isinstance(url, unicode):
650
url = url.encode('utf-8')
651
(scheme, netloc, path, params,
652
query, fragment) = urlparse.urlparse(url, allow_fragments=False)
653
assert scheme == 'sftp'
654
username = password = host = port = None
656
username, host = netloc.split('@', 1)
658
username, password = username.split(':', 1)
659
password = urllib.unquote(password)
660
username = urllib.unquote(username)
665
host, port = host.rsplit(':', 1)
669
# TODO: Should this be ConnectionError?
670
raise TransportError('%s: invalid port number' % port)
671
host = urllib.unquote(host)
673
path = urllib.unquote(path)
675
# the initial slash should be removed from the path, and treated
676
# as a homedir relative path (the path begins with a double slash
677
# if it is absolute).
678
# see draft-ietf-secsh-scp-sftp-ssh-uri-03.txt
679
# RBC 20060118 we are not using this as its too user hostile. instead
680
# we are following lftp and using /~/foo to mean '~/foo'.
681
# handle homedir paths
682
if path.startswith('/~/'):
686
return (username, password, host, port, path)
688
def _parse_url(self, url):
689
(self._username, self._password,
690
self._host, self._port, self._path) = self._split_url(url)
692
def _sftp_connect(self):
693
"""Connect to the remote sftp server.
694
After this, self._sftp should have a valid connection (or
695
we raise an TransportError 'could not connect').
697
TODO: Raise a more reasonable ConnectionFailed exception
699
global _connected_hosts
701
idx = (self._host, self._port, self._username)
703
self._sftp = _connected_hosts[idx]
708
vendor = _get_ssh_vendor()
709
if vendor == 'loopback':
710
sock = socket.socket()
711
sock.connect((self._host, self._port))
712
self._sftp = SFTPClient(LoopbackSFTP(sock))
713
elif vendor != 'none':
714
sock = SFTPSubprocess(self._host, vendor, self._port,
716
self._sftp = SFTPClient(sock)
718
self._paramiko_connect()
720
_connected_hosts[idx] = self._sftp
722
def _paramiko_connect(self):
723
global SYSTEM_HOSTKEYS, BZR_HOSTKEYS
728
t = paramiko.Transport((self._host, self._port or 22))
729
t.set_log_channel('bzr.paramiko')
731
except paramiko.SSHException, e:
732
raise ConnectionError('Unable to reach SSH host %s:%d' %
733
(self._host, self._port), e)
735
server_key = t.get_remote_server_key()
736
server_key_hex = paramiko.util.hexify(server_key.get_fingerprint())
737
keytype = server_key.get_name()
738
if SYSTEM_HOSTKEYS.has_key(self._host) and SYSTEM_HOSTKEYS[self._host].has_key(keytype):
739
our_server_key = SYSTEM_HOSTKEYS[self._host][keytype]
740
our_server_key_hex = paramiko.util.hexify(our_server_key.get_fingerprint())
741
elif BZR_HOSTKEYS.has_key(self._host) and BZR_HOSTKEYS[self._host].has_key(keytype):
742
our_server_key = BZR_HOSTKEYS[self._host][keytype]
743
our_server_key_hex = paramiko.util.hexify(our_server_key.get_fingerprint())
745
warning('Adding %s host key for %s: %s' % (keytype, self._host, server_key_hex))
746
if not BZR_HOSTKEYS.has_key(self._host):
747
BZR_HOSTKEYS[self._host] = {}
748
BZR_HOSTKEYS[self._host][keytype] = server_key
749
our_server_key = server_key
750
our_server_key_hex = paramiko.util.hexify(our_server_key.get_fingerprint())
752
if server_key != our_server_key:
753
filename1 = os.path.expanduser('~/.ssh/known_hosts')
754
filename2 = pathjoin(config_dir(), 'ssh_host_keys')
755
raise TransportError('Host keys for %s do not match! %s != %s' % \
756
(self._host, our_server_key_hex, server_key_hex),
757
['Try editing %s or %s' % (filename1, filename2)])
762
self._sftp = t.open_sftp_client()
763
except paramiko.SSHException, e:
764
raise ConnectionError('Unable to start sftp client %s:%d' %
765
(self._host, self._port), e)
767
def _sftp_auth(self, transport):
768
# paramiko requires a username, but it might be none if nothing was supplied
769
# use the local username, just in case.
770
# We don't override self._username, because if we aren't using paramiko,
771
# the username might be specified in ~/.ssh/config and we don't want to
772
# force it to something else
773
# Also, it would mess up the self.relpath() functionality
774
username = self._username or getpass.getuser()
776
# Paramiko tries to open a socket.AF_UNIX in order to connect
777
# to ssh-agent. That attribute doesn't exist on win32 (it does in cygwin)
778
# so we get an AttributeError exception. For now, just don't try to
779
# connect to an agent if we are on win32
780
if sys.platform != 'win32':
781
agent = paramiko.Agent()
782
for key in agent.get_keys():
783
mutter('Trying SSH agent key %s' % paramiko.util.hexify(key.get_fingerprint()))
785
transport.auth_publickey(username, key)
787
except paramiko.SSHException, e:
790
# okay, try finding id_rsa or id_dss? (posix only)
791
if self._try_pkey_auth(transport, paramiko.RSAKey, username, 'id_rsa'):
793
if self._try_pkey_auth(transport, paramiko.DSSKey, username, 'id_dsa'):
798
transport.auth_password(username, self._password)
800
except paramiko.SSHException, e:
803
# FIXME: Don't keep a password held in memory if you can help it
804
#self._password = None
806
# give up and ask for a password
807
password = bzrlib.ui.ui_factory.get_password(
808
prompt='SSH %(user)s@%(host)s password',
809
user=username, host=self._host)
811
transport.auth_password(username, password)
812
except paramiko.SSHException, e:
813
raise ConnectionError('Unable to authenticate to SSH host as %s@%s' %
814
(username, self._host), e)
816
def _try_pkey_auth(self, transport, pkey_class, username, filename):
817
filename = os.path.expanduser('~/.ssh/' + filename)
819
key = pkey_class.from_private_key_file(filename)
820
transport.auth_publickey(username, key)
822
except paramiko.PasswordRequiredException:
823
password = bzrlib.ui.ui_factory.get_password(
824
prompt='SSH %(filename)s password',
827
key = pkey_class.from_private_key_file(filename, password)
828
transport.auth_publickey(username, key)
830
except paramiko.SSHException:
831
mutter('SSH authentication via %s key failed.' % (os.path.basename(filename),))
832
except paramiko.SSHException:
833
mutter('SSH authentication via %s key failed.' % (os.path.basename(filename),))
838
def _sftp_open_exclusive(self, abspath, mode=None):
839
"""Open a remote path exclusively.
841
SFTP supports O_EXCL (SFTP_FLAG_EXCL), which fails if
842
the file already exists. However it does not expose this
843
at the higher level of SFTPClient.open(), so we have to
846
WARNING: This breaks the SFTPClient abstraction, so it
847
could easily break against an updated version of paramiko.
849
:param abspath: The remote absolute path where the file should be opened
850
:param mode: The mode permissions bits for the new file
852
path = self._sftp._adjust_cwd(abspath)
853
attr = SFTPAttributes()
856
omode = (SFTP_FLAG_WRITE | SFTP_FLAG_CREATE
857
| SFTP_FLAG_TRUNC | SFTP_FLAG_EXCL)
859
t, msg = self._sftp._request(CMD_OPEN, path, omode, attr)
861
raise TransportError('Expected an SFTP handle')
862
handle = msg.get_string()
863
return SFTPFile(self._sftp, handle, 'wb', -1)
864
except (paramiko.SSHException, IOError), e:
865
self._translate_io_exception(e, abspath, ': unable to open',
866
failure_exc=FileExists)
869
# ------------- server test implementation --------------
873
from bzrlib.tests.stub_sftp import StubServer, StubSFTPServer
875
STUB_SERVER_KEY = """
876
-----BEGIN RSA PRIVATE KEY-----
877
MIICWgIBAAKBgQDTj1bqB4WmayWNPB+8jVSYpZYk80Ujvj680pOTh2bORBjbIAyz
878
oWGW+GUjzKxTiiPvVmxFgx5wdsFvF03v34lEVVhMpouqPAYQ15N37K/ir5XY+9m/
879
d8ufMCkjeXsQkKqFbAlQcnWMCRnOoPHS3I4vi6hmnDDeeYTSRvfLbW0fhwIBIwKB
880
gBIiOqZYaoqbeD9OS9z2K9KR2atlTxGxOJPXiP4ESqP3NVScWNwyZ3NXHpyrJLa0
881
EbVtzsQhLn6rF+TzXnOlcipFvjsem3iYzCpuChfGQ6SovTcOjHV9z+hnpXvQ/fon
882
soVRZY65wKnF7IAoUwTmJS9opqgrN6kRgCd3DASAMd1bAkEA96SBVWFt/fJBNJ9H
883
tYnBKZGw0VeHOYmVYbvMSstssn8un+pQpUm9vlG/bp7Oxd/m+b9KWEh2xPfv6zqU
884
avNwHwJBANqzGZa/EpzF4J8pGti7oIAPUIDGMtfIcmqNXVMckrmzQ2vTfqtkEZsA
885
4rE1IERRyiJQx6EJsz21wJmGV9WJQ5kCQQDwkS0uXqVdFzgHO6S++tjmjYcxwr3g
886
H0CoFYSgbddOT6miqRskOQF3DZVkJT3kyuBgU2zKygz52ukQZMqxCb1fAkASvuTv
887
qfpH87Qq5kQhNKdbbwbmd2NxlNabazPijWuphGTdW0VfJdWfklyS2Kr+iqrs/5wV
888
HhathJt636Eg7oIjAkA8ht3MQ+XSl9yIJIS8gVpbPxSw5OMfw0PjVE7tBdQruiSc
889
nvuQES5C9BMHjF39LZiGH1iLQy7FgdHyoP+eodI7
890
-----END RSA PRIVATE KEY-----
894
class SingleListener(threading.Thread):
896
def __init__(self, callback):
897
threading.Thread.__init__(self)
898
self._callback = callback
899
self._socket = socket.socket()
900
self._socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
901
self._socket.bind(('localhost', 0))
902
self._socket.listen(1)
903
self.port = self._socket.getsockname()[1]
904
self.stop_event = threading.Event()
907
s, _ = self._socket.accept()
908
# now close the listen socket
911
self._callback(s, self.stop_event)
913
pass #Ignore socket errors
915
# probably a failed test
916
warning('Exception from within unit test server thread: %r' % x)
919
self.stop_event.set()
920
# use a timeout here, because if the test fails, the server thread may
921
# never notice the stop_event.
925
class SFTPServer(Server):
926
"""Common code for SFTP server facilities."""
929
self._original_vendor = None
931
self._server_homedir = None
932
self._listener = None
934
self._vendor = 'none'
938
def _get_sftp_url(self, path):
939
"""Calculate an sftp url to this server for path."""
940
return 'sftp://foo:bar@localhost:%d/%s' % (self._listener.port, path)
942
def log(self, message):
943
"""StubServer uses this to log when a new server is created."""
944
self.logs.append(message)
946
def _run_server(self, s, stop_event):
947
ssh_server = paramiko.Transport(s)
948
key_file = os.path.join(self._homedir, 'test_rsa.key')
949
file(key_file, 'w').write(STUB_SERVER_KEY)
950
host_key = paramiko.RSAKey.from_private_key_file(key_file)
951
ssh_server.add_server_key(host_key)
952
server = StubServer(self)
953
ssh_server.set_subsystem_handler('sftp', paramiko.SFTPServer,
954
StubSFTPServer, root=self._root,
955
home=self._server_homedir)
956
event = threading.Event()
957
ssh_server.start_server(event, server)
959
stop_event.wait(30.0)
963
self._original_vendor = _ssh_vendor
964
_ssh_vendor = self._vendor
965
self._homedir = os.getcwdu()
966
if self._server_homedir is None:
967
self._server_homedir = self._homedir
969
# FIXME WINDOWS: _root should be _server_homedir[0]:/
970
self._listener = SingleListener(self._run_server)
971
self._listener.setDaemon(True)
972
self._listener.start()
975
"""See bzrlib.transport.Server.tearDown."""
977
self._listener.stop()
978
_ssh_vendor = self._original_vendor
981
class SFTPFullAbsoluteServer(SFTPServer):
982
"""A test server for sftp transports, using absolute urls and ssh."""
985
"""See bzrlib.transport.Server.get_url."""
986
return self._get_sftp_url(urlescape(self._homedir[1:]))
989
class SFTPServerWithoutSSH(SFTPServer):
990
"""An SFTP server that uses a simple TCP socket pair rather than SSH."""
993
super(SFTPServerWithoutSSH, self).__init__()
994
self._vendor = 'loopback'
996
def _run_server(self, sock, stop_event):
997
class FakeChannel(object):
998
def get_transport(self):
1000
def get_log_channel(self):
1004
def get_hexdump(self):
1007
server = paramiko.SFTPServer(FakeChannel(), 'sftp', StubServer(self), StubSFTPServer,
1008
root=self._root, home=self._server_homedir)
1009
server.start_subsystem('sftp', None, sock)
1010
server.finish_subsystem()
1013
class SFTPAbsoluteServer(SFTPServerWithoutSSH):
1014
"""A test server for sftp transports, using absolute urls."""
1017
"""See bzrlib.transport.Server.get_url."""
1018
return self._get_sftp_url(urlescape(self._homedir[1:]))
1021
class SFTPHomeDirServer(SFTPServerWithoutSSH):
1022
"""A test server for sftp transports, using homedir relative urls."""
1025
"""See bzrlib.transport.Server.get_url."""
1026
return self._get_sftp_url("~/")
1029
class SFTPSiblingAbsoluteServer(SFTPAbsoluteServer):
1030
"""A test servere for sftp transports, using absolute urls to non-home."""
1033
self._server_homedir = '/dev/noone/runs/tests/here'
1034
super(SFTPSiblingAbsoluteServer, self).setUp()
1037
def get_test_permutations():
1038
"""Return the permutations to be used in testing."""
1039
return [(SFTPTransport, SFTPAbsoluteServer),
1040
(SFTPTransport, SFTPHomeDirServer),
1041
(SFTPTransport, SFTPSiblingAbsoluteServer),