Skip to content

Commit 193ae31

Browse files
committed
Added sftp server interface stub by https://github.com/rspivak/sftpserver. Added stub SFTP handler to fake server. Added SFTP functionality tests for SSHClient. Cleaned up fake server startup, doc changes
1 parent ad0880f commit 193ae31

File tree

4 files changed

+264
-58
lines changed

4 files changed

+264
-58
lines changed

fake_server/fake_server.py

Lines changed: 16 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
import logging
2020
import paramiko
2121
import time
22+
from stub_sftp import StubSFTPServer
2223

2324
logger = logging.getLogger("fake_server")
2425
paramiko_logger = logging.getLogger('paramiko.transport')
@@ -81,70 +82,56 @@ def listen(cmd_req_response, sock, fail_auth = False):
8182
response to client connection. Returns (server, socket) tuple \
8283
where server is a joinable server thread and socket is listening \
8384
socket of server."""
84-
# sock = _make_socket(listen_ip)
8585
listen_ip, listen_port = sock.getsockname()
8686
if not sock:
8787
logger.error("Could not establish listening connection on %s:%s", listen_ip, listen_port)
8888
return
8989
try:
9090
sock.listen(100)
9191
logger.info('Listening for connection on %s:%s..', listen_ip, listen_port)
92-
# client, addr = sock.accept()
9392
except Exception, e:
9493
logger.error('*** Listen failed: %s' % (str(e),))
9594
traceback.print_exc()
9695
return
97-
# accept_thread = gevent.spawn(handle_ssh_connection,
98-
# cmd_req_response, client, addr,
99-
# fail_auth=fail_auth)
10096
handle_ssh_connection(cmd_req_response, sock, fail_auth=fail_auth)
101-
# accept_thread.start()
102-
# return accept_thread
10397

104-
def _handle_ssh_connection(cmd_req_response, t, fail_auth = False):
98+
def _handle_ssh_connection(cmd_req_response, transport, fail_auth = False):
10599
try:
106-
t.load_server_moduli()
100+
transport.load_server_moduli()
107101
except:
108102
return
109-
t.add_server_key(host_key)
103+
transport.add_server_key(host_key)
104+
transport.set_subsystem_handler('sftp', paramiko.SFTPServer, StubSFTPServer)
110105
server = Server(cmd_req_response = cmd_req_response, fail_auth = fail_auth)
111106
try:
112-
t.start_server(server=server)
107+
transport.start_server(server=server)
113108
except paramiko.SSHException, e:
114109
logger.exception('SSH negotiation failed')
115110
return
116111
except Exception:
117112
logger.exception("Error occured starting server")
118113
return
119-
return _accept_ssh_data(t, server)
120-
121-
def _accept_ssh_data(t, server):
122-
chan = t.accept(20)
123-
if not chan:
114+
channel = transport.accept(20)
115+
if not channel:
124116
logger.error("Could not establish channel")
125117
return
126-
logger.info("Authenticated..")
127-
chan.send_ready()
128-
server.event.wait(10)
129-
if not server.event.isSet():
130-
logger.error('Client never sent command')
131-
chan.close()
132-
return
133-
while not chan.send_ready():
118+
while transport.is_active():
119+
time.sleep(1)
120+
while not channel.send_ready():
134121
time.sleep(.5)
135-
chan.close()
122+
channel.close()
136123

137124
def handle_ssh_connection(cmd_req_response, sock, fail_auth = False):
138-
client, addr = sock.accept()
125+
conn, addr = sock.accept()
139126
logger.info('Got connection..')
140127
try:
141-
t = paramiko.Transport(client)
142-
_handle_ssh_connection(cmd_req_response, t, fail_auth=fail_auth)
128+
transport = paramiko.Transport(conn)
129+
_handle_ssh_connection(cmd_req_response, transport, fail_auth=fail_auth)
143130
except Exception, e:
144131
logger.error('*** Caught exception: %s: %s' % (str(e.__class__), str(e),))
145132
traceback.print_exc()
146133
try:
147-
t.close()
134+
transport.close()
148135
except:
149136
pass
150137
return

fake_server/stub_sftp.py

Lines changed: 206 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,206 @@
1+
# Copyright (C) 2003-2009 Robey Pointer <robeypointer@gmail.com>
2+
#
3+
# This file is part of paramiko.
4+
#
5+
# Paramiko is free software; you can redistribute it and/or modify it under the
6+
# terms of the GNU Lesser General Public License as published by the Free
7+
# Software Foundation; either version 2.1 of the License, or (at your option)
8+
# any later version.
9+
#
10+
# Paramiko is distrubuted in the hope that it will be useful, but WITHOUT ANY
11+
# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
12+
# A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more
13+
# details.
14+
#
15+
# You should have received a copy of the GNU Lesser General Public License
16+
# along with Paramiko; if not, write to the Free Software Foundation, Inc.,
17+
# 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA.
18+
19+
"""
20+
A stub SFTP server for loopback SFTP testing.
21+
"""
22+
23+
import os
24+
from paramiko import ServerInterface, SFTPServerInterface, SFTPServer, SFTPAttributes, \
25+
SFTPHandle, SFTP_OK, AUTH_SUCCESSFUL, OPEN_SUCCEEDED
26+
27+
28+
class StubServer (ServerInterface):
29+
def check_auth_password(self, username, password):
30+
# all are allowed
31+
return AUTH_SUCCESSFUL
32+
33+
def check_channel_request(self, kind, chanid):
34+
return OPEN_SUCCEEDED
35+
36+
37+
class StubSFTPHandle (SFTPHandle):
38+
def stat(self):
39+
try:
40+
return SFTPAttributes.from_stat(os.fstat(self.readfile.fileno()))
41+
except OSError as e:
42+
return SFTPServer.convert_errno(e.errno)
43+
44+
def chattr(self, attr):
45+
# python doesn't have equivalents to fchown or fchmod, so we have to
46+
# use the stored filename
47+
try:
48+
SFTPServer.set_file_attr(self.filename, attr)
49+
return SFTP_OK
50+
except OSError as e:
51+
return SFTPServer.convert_errno(e.errno)
52+
53+
54+
class StubSFTPServer (SFTPServerInterface):
55+
# assume current folder is a fine root
56+
# (the tests always create and eventualy delete a subfolder, so there shouldn't be any mess)
57+
ROOT = os.getcwd()
58+
59+
def _realpath(self, path):
60+
return self.ROOT + self.canonicalize(path)
61+
62+
def list_folder(self, path):
63+
path = self._realpath(path)
64+
try:
65+
out = [ ]
66+
flist = os.listdir(path)
67+
for fname in flist:
68+
attr = SFTPAttributes.from_stat(os.stat(os.path.join(path, fname)))
69+
attr.filename = fname
70+
out.append(attr)
71+
return out
72+
except OSError as e:
73+
return SFTPServer.convert_errno(e.errno)
74+
75+
def stat(self, path):
76+
path = self._realpath(path)
77+
try:
78+
return SFTPAttributes.from_stat(os.stat(path))
79+
except OSError as e:
80+
return SFTPServer.convert_errno(e.errno)
81+
82+
def lstat(self, path):
83+
path = self._realpath(path)
84+
try:
85+
return SFTPAttributes.from_stat(os.lstat(path))
86+
except OSError as e:
87+
return SFTPServer.convert_errno(e.errno)
88+
89+
def open(self, path, flags, attr):
90+
path = self._realpath(path)
91+
try:
92+
binary_flag = getattr(os, 'O_BINARY', 0)
93+
flags |= binary_flag
94+
mode = getattr(attr, 'st_mode', None)
95+
if mode is not None:
96+
fd = os.open(path, flags, mode)
97+
else:
98+
# os.open() defaults to 0777 which is
99+
# an odd default mode for files
100+
fd = os.open(path, flags, 0o666)
101+
except OSError as e:
102+
return SFTPServer.convert_errno(e.errno)
103+
if (flags & os.O_CREAT) and (attr is not None):
104+
attr._flags &= ~attr.FLAG_PERMISSIONS
105+
SFTPServer.set_file_attr(path, attr)
106+
if flags & os.O_WRONLY:
107+
if flags & os.O_APPEND:
108+
fstr = 'ab'
109+
else:
110+
fstr = 'wb'
111+
elif flags & os.O_RDWR:
112+
if flags & os.O_APPEND:
113+
fstr = 'a+b'
114+
else:
115+
fstr = 'r+b'
116+
else:
117+
# O_RDONLY (== 0)
118+
fstr = 'rb'
119+
try:
120+
f = os.fdopen(fd, fstr)
121+
except OSError as e:
122+
return SFTPServer.convert_errno(e.errno)
123+
fobj = StubSFTPHandle(flags)
124+
fobj.filename = path
125+
fobj.readfile = f
126+
fobj.writefile = f
127+
return fobj
128+
129+
def remove(self, path):
130+
path = self._realpath(path)
131+
try:
132+
os.remove(path)
133+
except OSError as e:
134+
return SFTPServer.convert_errno(e.errno)
135+
return SFTP_OK
136+
137+
def rename(self, oldpath, newpath):
138+
oldpath = self._realpath(oldpath)
139+
newpath = self._realpath(newpath)
140+
try:
141+
os.rename(oldpath, newpath)
142+
except OSError as e:
143+
return SFTPServer.convert_errno(e.errno)
144+
return SFTP_OK
145+
146+
def mkdir(self, path, attr):
147+
path = self._realpath(path)
148+
try:
149+
os.mkdir(path)
150+
if attr is not None:
151+
SFTPServer.set_file_attr(path, attr)
152+
except OSError as e:
153+
return SFTPServer.convert_errno(e.errno)
154+
return SFTP_OK
155+
156+
def rmdir(self, path):
157+
path = self._realpath(path)
158+
try:
159+
os.rmdir(path)
160+
except OSError as e:
161+
return SFTPServer.convert_errno(e.errno)
162+
return SFTP_OK
163+
164+
def chattr(self, path, attr):
165+
path = self._realpath(path)
166+
try:
167+
SFTPServer.set_file_attr(path, attr)
168+
except OSError as e:
169+
return SFTPServer.convert_errno(e.errno)
170+
return SFTP_OK
171+
172+
def symlink(self, target_path, path):
173+
path = self._realpath(path)
174+
if (len(target_path) > 0) and (target_path[0] == '/'):
175+
# absolute symlink
176+
target_path = os.path.join(self.ROOT, target_path[1:])
177+
if target_path[:2] == '//':
178+
# bug in os.path.join
179+
target_path = target_path[1:]
180+
else:
181+
# compute relative to path
182+
abspath = os.path.join(os.path.dirname(path), target_path)
183+
if abspath[:len(self.ROOT)] != self.ROOT:
184+
# this symlink isn't going to work anyway -- just break it immediately
185+
target_path = '<error>'
186+
try:
187+
os.symlink(target_path, path)
188+
except OSError as e:
189+
return SFTPServer.convert_errno(e.errno)
190+
return SFTP_OK
191+
192+
def readlink(self, path):
193+
path = self._realpath(path)
194+
try:
195+
symlink = os.readlink(path)
196+
except OSError as e:
197+
return SFTPServer.convert_errno(e.errno)
198+
# if it's absolute, remove the root
199+
if os.path.isabs(symlink):
200+
if symlink[:len(self.ROOT)] == self.ROOT:
201+
symlink = symlink[len(self.ROOT):]
202+
if (len(symlink) == 0) or (symlink[0] != '/'):
203+
symlink = '/' + symlink
204+
else:
205+
symlink = '<error>'
206+
return symlink

pssh.py

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -226,8 +226,6 @@ def __init__(self, hosts,
226226
"""
227227
:param hosts: Hosts to connect to
228228
:type hosts: list(str)
229-
:param pool_size: Pool size - how many commands to run in parallel
230-
:type pool_size: int
231229
:param user: (Optional) User to login as. Defaults to logged in user or\
232230
user from ~/.ssh/config or /etc/ssh/ssh_config if set
233231
:type user: str
@@ -239,8 +237,7 @@ def __init__(self, hosts,
239237
:type port: int
240238
:param pkey: (Optional) Client's private key to be used to connect with
241239
:type pkey: :mod:`paramiko.PKey`
242-
:param pool_size: (Optional) Greenlet pool size. Controls on how many\
243-
hosts to execute tasks in parallel. Defaults to 10
240+
:param pool_size: Pool size - how many commands to run in parallel
244241
:type pool_size: int
245242
:raises: :mod:`pssh.AuthenticationException` on authentication error
246243
:raises: :mod:`pssh.UnknownHostException` on DNS resolution error

tests/test_ssh_client.py

Lines changed: 41 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -3,34 +3,50 @@
33
"""Unittests for parallel-ssh"""
44

55
import unittest
6-
from pssh import SSHClient, ParallelSSHClient, UnknownHostException
7-
from paramiko import AuthenticationException
6+
from pssh import SSHClient, ParallelSSHClient, UnknownHostException, AuthenticationException, _setup_logger, logger
7+
from fake_server.fake_server import start_server, make_socket, logger as server_logger, \
8+
paramiko_logger
9+
import os
810

9-
class SSHClientTest(unittest.TestCase):
10-
11-
def test_ssh_client(self):
12-
try:
13-
client = SSHClient('testy')
14-
except UnknownHostException, e:
15-
print e
16-
return
17-
stdin, stdout, stderr = client.exec_command('ls -ltrh')
18-
for line in stdout:
19-
print line.strip()
20-
client.copy_file("fake file", "fake file")
11+
# _setup_logger(server_logger)
12+
# _setup_logger(logger)
13+
# _setup_logger(paramiko_logger)
2114

22-
class ParallelSSHClientTest(unittest.TestCase):
15+
class SSHClientTest(unittest.TestCase):
16+
17+
def setUp(self):
18+
self.fake_cmd = 'fake cmd'
19+
self.fake_resp = 'fake response'
20+
self.listen_socket = make_socket('127.0.0.1')
21+
self.listen_port = self.listen_socket.getsockname()[1]
2322

24-
def test_parallel_ssh_client(self):
25-
client = ParallelSSHClient(['testy'])
26-
cmds = client.exec_command('ls -ltrh')
27-
try:
28-
print [client.get_stdout(cmd) for cmd in cmds]
29-
except UnknownHostException, e:
30-
print e
31-
return
32-
cmds = client.copy_file('fake file', 'fake file')
33-
client.pool.join()
23+
def test_ssh_client_sftp(self):
24+
"""Test SFTP features of SSHClient. Copy local filename to server,
25+
check that data in both files is the same, make new directory on
26+
server, remove files and directory."""
27+
test_file_data = 'test'
28+
local_filename = 'test_file'
29+
remote_filename = 'test_file_copy'
30+
remote_dir = 'remote_dir'
31+
test_file = open(local_filename, 'w')
32+
test_file.writelines([test_file_data + os.linesep])
33+
test_file.close()
34+
server = start_server({ self.fake_cmd : self.fake_resp }, self.listen_socket)
35+
client = SSHClient('127.0.0.1', port=self.listen_port)
36+
client.copy_file(local_filename, remote_filename)
37+
self.assertTrue(os.path.isfile(remote_filename), msg="SFTP copy failed")
38+
copied_file = open(remote_filename, 'r')
39+
copied_file_data = copied_file.readlines()[0].strip()
40+
copied_file.close()
41+
self.assertEqual(test_file_data, copied_file_data,
42+
msg="Data in destination file %s does not match source %s" % (copied_file_data, test_file_data))
43+
os.unlink(local_filename)
44+
os.unlink(remote_filename)
45+
client.mkdir(client._make_sftp(), remote_dir)
46+
self.assertTrue(os.path.isdir(remote_dir))
47+
os.rmdir(remote_dir)
48+
del client
49+
server.join()
3450

3551
if __name__ == '__main__':
3652
unittest.main()

0 commit comments

Comments
 (0)