Status: | Merged |
---|---|
Approved by: | Jelmer Vernooij |
Approved revision: | no longer in the source branch. |
Merge reported by: | The Breezy Bot |
Merged at revision: | not available |
Proposed branch: | lp:~jelmer/brz/medusa |
Merge into: | lp:brz |
Diff against target: |
550 lines (+56/-349) 5 files modified
breezy/tests/ftp_server/__init__.py (+6/-24) breezy/tests/ftp_server/medusa_based.py (+0/-295) breezy/tests/ftp_server/pyftpdlib_based.py (+44/-29) doc/en/release-notes/brz-3.0.txt (+6/-0) setup.py (+0/-1) |
To merge this branch: | bzr merge lp:~jelmer/brz/medusa |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Martin Packman | Approve | ||
Review via email: mp+325457@code.launchpad.net |
Commit message
Drop support for medusa, support newer versions of pyftpdlib.
Description of the change
Drop support for medusa.
Support newer versions of pyftpdlib.
To post a comment you must log in.
Preview Diff
[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1 | === modified file 'breezy/tests/ftp_server/__init__.py' |
2 | --- breezy/tests/ftp_server/__init__.py 2017-05-21 18:10:28 +0000 |
3 | +++ breezy/tests/ftp_server/__init__.py 2017-06-11 13:38:25 +0000 |
4 | @@ -27,27 +27,11 @@ |
5 | |
6 | |
7 | try: |
8 | - from breezy.tests.ftp_server import medusa_based |
9 | - # medusa is bogus starting with python2.6, since we don't support earlier |
10 | - # pythons anymore, it's currently useless. There is hope though that the |
11 | - # unicode bugs get fixed in the future so we leave it disabled until |
12 | - # then. Keeping the framework in place means that only the following line |
13 | - # will need to be changed. The last tests were conducted with medusa-2.0 |
14 | - # -- vila 20110607 |
15 | - medusa_available = False |
16 | -except ImportError: |
17 | - medusa_available = False |
18 | - |
19 | - |
20 | -try: |
21 | - from breezy.tests.ftp_server import pyftpdlib_based |
22 | - if pyftpdlib_based.pyftplib_version >= (0, 7, 0): |
23 | - pyftpdlib_available = True |
24 | - else: |
25 | - # 0.6.0 breaks SITE CHMOD |
26 | - pyftpdlib_available = False |
27 | + import pyftpdlib |
28 | except ImportError: |
29 | pyftpdlib_available = False |
30 | +else: |
31 | + pyftpdlib_available = True |
32 | |
33 | |
34 | class _FTPServerFeature(features.Feature): |
35 | @@ -56,12 +40,11 @@ |
36 | Right now, the only way this is available is if one of the following is |
37 | installed: |
38 | |
39 | - - 'medusa': http://www.amk.ca/python/code/medusa.html |
40 | - 'pyftpdlib': http://code.google.com/p/pyftpdlib/ |
41 | """ |
42 | |
43 | def _probe(self): |
44 | - return medusa_available or pyftpdlib_available |
45 | + return pyftpdlib_available |
46 | |
47 | def feature_name(self): |
48 | return 'FTPServer' |
49 | @@ -92,9 +75,8 @@ |
50 | raise tests.UnavailableFeature(FTPServerFeature) |
51 | |
52 | |
53 | -if medusa_available: |
54 | - FTPTestServer = medusa_based.FTPTestServer |
55 | -elif pyftpdlib_available: |
56 | +if pyftpdlib_available: |
57 | + from . import pyftpdlib_based |
58 | FTPTestServer = pyftpdlib_based.FTPTestServer |
59 | else: |
60 | FTPTestServer = UnavailableFTPTestServer |
61 | |
62 | === removed file 'breezy/tests/ftp_server/medusa_based.py' |
63 | --- breezy/tests/ftp_server/medusa_based.py 2017-05-22 00:56:52 +0000 |
64 | +++ breezy/tests/ftp_server/medusa_based.py 1970-01-01 00:00:00 +0000 |
65 | @@ -1,295 +0,0 @@ |
66 | -# Copyright (C) 2007-2010 Canonical Ltd |
67 | -# |
68 | -# This program is free software; you can redistribute it and/or modify |
69 | -# it under the terms of the GNU General Public License as published by |
70 | -# the Free Software Foundation; either version 2 of the License, or |
71 | -# (at your option) any later version. |
72 | -# |
73 | -# This program is distributed in the hope that it will be useful, |
74 | -# but WITHOUT ANY WARRANTY; without even the implied warranty of |
75 | -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
76 | -# GNU General Public License for more details. |
77 | -# |
78 | -# You should have received a copy of the GNU General Public License |
79 | -# along with this program; if not, write to the Free Software |
80 | -# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA |
81 | -""" |
82 | -FTP test server. |
83 | - |
84 | -Based on medusa: http://www.amk.ca/python/code/medusa.html |
85 | -""" |
86 | - |
87 | -import asyncore |
88 | -import errno |
89 | -import os |
90 | -import select |
91 | -import stat |
92 | -import threading |
93 | - |
94 | -import medusa |
95 | -import medusa.filesys |
96 | -import medusa.ftp_server |
97 | - |
98 | -from ... import ( |
99 | - osutils, |
100 | - tests, |
101 | - trace, |
102 | - ) |
103 | -from .. import test_server |
104 | - |
105 | - |
106 | -class test_filesystem(medusa.filesys.os_filesystem): |
107 | - """A custom filesystem wrapper to add missing functionalities.""" |
108 | - |
109 | - def chmod(self, path, mode): |
110 | - p = self.normalize(self.path_module.join (self.wd, path)) |
111 | - return os.chmod(self.translate(p), mode) |
112 | - |
113 | - |
114 | -class test_authorizer(object): |
115 | - """A custom Authorizer object for running the test suite. |
116 | - |
117 | - The reason we cannot use dummy_authorizer, is because it sets the |
118 | - channel to readonly, which we don't always want to do. |
119 | - """ |
120 | - |
121 | - def __init__(self, root): |
122 | - self.root = root |
123 | - # If secured_user is set secured_password will be checked |
124 | - self.secured_user = None |
125 | - self.secured_password = None |
126 | - |
127 | - def authorize(self, channel, username, password): |
128 | - """Return (success, reply_string, filesystem)""" |
129 | - channel.persona = -1, -1 |
130 | - if username == 'anonymous': |
131 | - channel.read_only = 1 |
132 | - else: |
133 | - channel.read_only = 0 |
134 | - |
135 | - # Check secured_user if set |
136 | - if (self.secured_user is not None |
137 | - and username == self.secured_user |
138 | - and password != self.secured_password): |
139 | - return 0, 'Password invalid.', None |
140 | - else: |
141 | - return 1, 'OK.', test_filesystem(self.root) |
142 | - |
143 | - |
144 | -class ftp_channel(medusa.ftp_server.ftp_channel): |
145 | - """Customized ftp channel""" |
146 | - |
147 | - def log(self, message): |
148 | - """Redirect logging requests.""" |
149 | - trace.mutter('ftp_channel: %s', message) |
150 | - |
151 | - def log_info(self, message, type='info'): |
152 | - """Redirect logging requests.""" |
153 | - trace.mutter('ftp_channel %s: %s', type, message) |
154 | - |
155 | - def cmd_rnfr(self, line): |
156 | - """Prepare for renaming a file.""" |
157 | - self._renaming = line[1] |
158 | - self.respond('350 Ready for RNTO') |
159 | - # TODO: jam 20060516 in testing, the ftp server seems to |
160 | - # check that the file already exists, or it sends |
161 | - # 550 RNFR command failed |
162 | - |
163 | - def cmd_rnto(self, line): |
164 | - """Rename a file based on the target given. |
165 | - |
166 | - rnto must be called after calling rnfr. |
167 | - """ |
168 | - if not self._renaming: |
169 | - self.respond('503 RNFR required first.') |
170 | - pfrom = self.filesystem.translate(self._renaming) |
171 | - self._renaming = None |
172 | - pto = self.filesystem.translate(line[1]) |
173 | - if os.path.exists(pto): |
174 | - self.respond('550 RNTO failed: file exists') |
175 | - return |
176 | - try: |
177 | - os.rename(pfrom, pto) |
178 | - except (IOError, OSError) as e: |
179 | - # TODO: jam 20060516 return custom responses based on |
180 | - # why the command failed |
181 | - # (bialix 20070418) str(e) on Python 2.5 @ Windows |
182 | - # sometimes don't provide expected error message; |
183 | - # so we obtain such message via os.strerror() |
184 | - self.respond('550 RNTO failed: %s' % os.strerror(e.errno)) |
185 | - except: |
186 | - self.respond('550 RNTO failed') |
187 | - # For a test server, we will go ahead and just die |
188 | - raise |
189 | - else: |
190 | - self.respond('250 Rename successful.') |
191 | - |
192 | - def cmd_size(self, line): |
193 | - """Return the size of a file |
194 | - |
195 | - This is overloaded to help the test suite determine if the |
196 | - target is a directory. |
197 | - """ |
198 | - filename = line[1] |
199 | - if not self.filesystem.isfile(filename): |
200 | - if self.filesystem.isdir(filename): |
201 | - self.respond('550 "%s" is a directory' % (filename,)) |
202 | - else: |
203 | - self.respond('550 "%s" is not a file' % (filename,)) |
204 | - else: |
205 | - self.respond('213 %d' |
206 | - % (self.filesystem.stat(filename)[stat.ST_SIZE]),) |
207 | - |
208 | - def cmd_mkd(self, line): |
209 | - """Create a directory. |
210 | - |
211 | - Overloaded because default implementation does not distinguish |
212 | - *why* it cannot make a directory. |
213 | - """ |
214 | - if len (line) != 2: |
215 | - self.command_not_understood(''.join(line)) |
216 | - else: |
217 | - path = line[1] |
218 | - try: |
219 | - self.filesystem.mkdir (path) |
220 | - self.respond ('257 MKD command successful.') |
221 | - except (IOError, OSError) as e: |
222 | - # (bialix 20070418) str(e) on Python 2.5 @ Windows |
223 | - # sometimes don't provide expected error message; |
224 | - # so we obtain such message via os.strerror() |
225 | - self.respond ('550 error creating directory: %s' % |
226 | - os.strerror(e.errno)) |
227 | - except: |
228 | - self.respond ('550 error creating directory.') |
229 | - |
230 | - def cmd_site(self, line): |
231 | - """Site specific commands.""" |
232 | - command, args = line[1].split(' ', 1) |
233 | - if command.lower() == 'chmod': |
234 | - try: |
235 | - mode, path = args.split() |
236 | - mode = int(mode, 8) |
237 | - except ValueError: |
238 | - # We catch both malformed line and malformed mode with the same |
239 | - # ValueError. |
240 | - self.command_not_understood(' '.join(line)) |
241 | - return |
242 | - try: |
243 | - # Yes path and mode are reversed |
244 | - self.filesystem.chmod(path, mode) |
245 | - self.respond('200 SITE CHMOD command successful') |
246 | - except AttributeError: |
247 | - # The chmod method is not available in read-only and will raise |
248 | - # AttributeError since a different filesystem is used in that |
249 | - # case |
250 | - self.command_not_authorized(' '.join(line)) |
251 | - else: |
252 | - # Another site specific command was requested. We don't know that |
253 | - # one |
254 | - self.command_not_understood(' '.join(line)) |
255 | - |
256 | - |
257 | -class ftp_server(medusa.ftp_server.ftp_server): |
258 | - """Customize the behavior of the Medusa ftp_server. |
259 | - |
260 | - There are a few warts on the ftp_server, based on how it expects |
261 | - to be used. |
262 | - """ |
263 | - _renaming = None |
264 | - ftp_channel_class = ftp_channel |
265 | - |
266 | - def __init__(self, *args, **kwargs): |
267 | - trace.mutter('Initializing ftp_server: %r, %r', args, kwargs) |
268 | - medusa.ftp_server.ftp_server.__init__(self, *args, **kwargs) |
269 | - |
270 | - def log(self, message): |
271 | - """Redirect logging requests.""" |
272 | - trace.mutter('ftp_server: %s', message) |
273 | - |
274 | - def log_info(self, message, type='info'): |
275 | - """Override the asyncore.log_info so we don't stipple the screen.""" |
276 | - trace.mutter('ftp_server %s: %s', type, message) |
277 | - |
278 | - |
279 | -class FTPTestServer(test_server.TestServer): |
280 | - """Common code for FTP server facilities.""" |
281 | - |
282 | - no_unicode_support = True |
283 | - |
284 | - def __init__(self): |
285 | - self._root = None |
286 | - self._ftp_server = None |
287 | - self._port = None |
288 | - self._async_thread = None |
289 | - # ftp server logs |
290 | - self.logs = [] |
291 | - |
292 | - def get_url(self): |
293 | - """Calculate an ftp url to this server.""" |
294 | - return 'ftp://foo:bar@localhost:%d/' % (self._port) |
295 | - |
296 | - def get_bogus_url(self): |
297 | - """Return a URL which cannot be connected to.""" |
298 | - return 'ftp://127.0.0.1:1' |
299 | - |
300 | - def log(self, message): |
301 | - """This is used by medusa.ftp_server to log connections, etc.""" |
302 | - self.logs.append(message) |
303 | - |
304 | - def start_server(self, vfs_server=None): |
305 | - if not (vfs_server is None or isinstance(vfs_server, |
306 | - test_server.LocalURLServer)): |
307 | - raise AssertionError( |
308 | - "FTPServer currently assumes local transport, got %s" % vfs_server) |
309 | - self._root = osutils.getcwd() |
310 | - self._ftp_server = ftp_server( |
311 | - authorizer=test_authorizer(root=self._root), |
312 | - ip='localhost', |
313 | - port=0, # bind to a random port |
314 | - resolver=None, |
315 | - logger_object=self # Use FTPServer.log() for messages |
316 | - ) |
317 | - self._port = self._ftp_server.getsockname()[1] |
318 | - # Don't let it loop forever, or handle an infinite number of requests. |
319 | - # In this case it will run for 1000s, or 10000 requests |
320 | - self._async_thread = threading.Thread( |
321 | - target=FTPTestServer._asyncore_loop_ignore_EBADF, |
322 | - kwargs={'timeout':0.1, 'count':10000}) |
323 | - if 'threads' in tests.selftest_debug_flags: |
324 | - sys.stderr.write('Thread started: %s\n' |
325 | - % (self._async_thread.ident,)) |
326 | - self._async_thread.setDaemon(True) |
327 | - self._async_thread.start() |
328 | - |
329 | - def stop_server(self): |
330 | - self._ftp_server.close() |
331 | - asyncore.close_all() |
332 | - self._async_thread.join() |
333 | - if 'threads' in tests.selftest_debug_flags: |
334 | - sys.stderr.write('Thread joined: %s\n' |
335 | - % (self._async_thread.ident,)) |
336 | - |
337 | - @staticmethod |
338 | - def _asyncore_loop_ignore_EBADF(*args, **kwargs): |
339 | - """Ignore EBADF during server shutdown. |
340 | - |
341 | - We close the socket to get the server to shutdown, but this causes |
342 | - select.select() to raise EBADF. |
343 | - """ |
344 | - try: |
345 | - asyncore.loop(*args, **kwargs) |
346 | - # FIXME: If we reach that point, we should raise an exception |
347 | - # explaining that the 'count' parameter in setUp is too low or |
348 | - # testers may wonder why their test just sits there waiting for a |
349 | - # server that is already dead. Note that if the tester waits too |
350 | - # long under pdb the server will also die. |
351 | - except select.error as e: |
352 | - if e.args[0] != errno.EBADF: |
353 | - raise |
354 | - |
355 | - def add_user(self, user, password): |
356 | - """Add a user with write access.""" |
357 | - authorizer = server = self._ftp_server.authorizer |
358 | - authorizer.secured_user = user |
359 | - authorizer.secured_password = password |
360 | - |
361 | |
362 | === modified file 'breezy/tests/ftp_server/pyftpdlib_based.py' |
363 | --- breezy/tests/ftp_server/pyftpdlib_based.py 2017-05-22 00:56:52 +0000 |
364 | +++ breezy/tests/ftp_server/pyftpdlib_based.py 2017-06-11 13:38:25 +0000 |
365 | @@ -20,8 +20,20 @@ |
366 | """ |
367 | |
368 | import errno |
369 | +import logging |
370 | import os |
371 | -from pyftpdlib import ftpserver |
372 | +import pyftpdlib |
373 | +import sys |
374 | +from pyftpdlib.authorizers import ( |
375 | + AuthorizerError, |
376 | + DummyAuthorizer, |
377 | + ) |
378 | +from pyftpdlib.filesystems import AbstractedFS |
379 | +from pyftpdlib.handlers import ( |
380 | + FTPHandler, |
381 | + proto_cmds, |
382 | + ) |
383 | +from pyftpdlib.servers import FTPServer |
384 | import select |
385 | import threading |
386 | |
387 | @@ -34,22 +46,31 @@ |
388 | from breezy.tests import test_server |
389 | |
390 | |
391 | +class NullHandler(logging.Handler): |
392 | + |
393 | + def emit(self, record): |
394 | + pass |
395 | + |
396 | +# Shut up very verbose pyftpdlib |
397 | +logging.getLogger('pyftpdlib').addHandler(NullHandler()) |
398 | + |
399 | + |
400 | # Convert the pyftplib string version into a tuple to avoid traps in string |
401 | # comparison. |
402 | -pyftplib_version = tuple(map(int, ftpserver.__ver__.split('.'))) |
403 | - |
404 | - |
405 | -class AnonymousWithWriteAccessAuthorizer(ftpserver.DummyAuthorizer): |
406 | +pyftplib_version = tuple(map(int, pyftpdlib.__ver__.split('.'))) |
407 | + |
408 | + |
409 | +class AnonymousWithWriteAccessAuthorizer(DummyAuthorizer): |
410 | |
411 | def _check_permissions(self, username, perm): |
412 | # Like base implementation but don't warn about write permissions |
413 | # assigned to anonymous, since that's exactly our purpose. |
414 | for p in perm: |
415 | if p not in self.read_perms + self.write_perms: |
416 | - raise ftpserver.AuthorizerError('No such permission "%s"' %p) |
417 | - |
418 | - |
419 | -class BzrConformingFS(ftpserver.AbstractedFS): |
420 | + raise AuthorizerError('No such permission "%s"' %p) |
421 | + |
422 | + |
423 | +class BzrConformingFS(AbstractedFS): |
424 | |
425 | def chmod(self, path, mode): |
426 | return os.chmod(path, mode) |
427 | @@ -59,19 +80,20 @@ |
428 | return [osutils.safe_utf8(s) for s in os.listdir(path)] |
429 | |
430 | def fs2ftp(self, fspath): |
431 | - p = ftpserver.AbstractedFS.fs2ftp(self, osutils.safe_unicode(fspath)) |
432 | + p = AbstractedFS.fs2ftp(self, osutils.safe_unicode(fspath)) |
433 | return osutils.safe_utf8(p) |
434 | |
435 | def ftp2fs(self, ftppath): |
436 | p = osutils.safe_unicode(ftppath) |
437 | - return ftpserver.AbstractedFS.ftp2fs(self, p) |
438 | - |
439 | -class BzrConformingFTPHandler(ftpserver.FTPHandler): |
440 | + return AbstractedFS.ftp2fs(self, p) |
441 | + |
442 | + |
443 | +class BzrConformingFTPHandler(FTPHandler): |
444 | |
445 | abstracted_fs = BzrConformingFS |
446 | |
447 | - def __init__(self, conn, server): |
448 | - ftpserver.FTPHandler.__init__(self, conn, server) |
449 | + def __init__(self, conn, server, ioloop=None): |
450 | + FTPHandler.__init__(self, conn, server) |
451 | self.authorizer = server.authorizer |
452 | |
453 | def ftp_SIZE(self, path): |
454 | @@ -83,7 +105,7 @@ |
455 | self.log('FAIL SIZE "%s". %s.' % (line, why)) |
456 | self.respond("550 %s." %why) |
457 | else: |
458 | - ftpserver.FTPHandler.ftp_SIZE(self, path) |
459 | + FTPHandler.ftp_SIZE(self, path) |
460 | |
461 | def ftp_NLST(self, path): |
462 | # bzr is overly picky here, but we want to make the test suite pass |
463 | @@ -94,7 +116,7 @@ |
464 | self.log('FAIL NLST "%s". %s.' % (line, why)) |
465 | self.respond("550 %s." %why) |
466 | else: |
467 | - ftpserver.FTPHandler.ftp_NLST(self, path) |
468 | + FTPHandler.ftp_NLST(self, path) |
469 | |
470 | def log_cmd(self, cmd, arg, respcode, respstr): |
471 | # base class version choke on unicode, the alternative is to just |
472 | @@ -107,12 +129,12 @@ |
473 | |
474 | |
475 | # An empty password is valid, hence the arg is neither mandatory nor forbidden |
476 | -ftpserver.proto_cmds['PASS']['arg'] = None |
477 | +proto_cmds['PASS']['arg'] = None |
478 | |
479 | -class ftp_server(ftpserver.FTPServer): |
480 | +class ftp_server(FTPServer): |
481 | |
482 | def __init__(self, address, handler, authorizer): |
483 | - ftpserver.FTPServer.__init__(self, address, handler) |
484 | + FTPServer.__init__(self, address, handler) |
485 | self.authorizer = authorizer |
486 | # Worth backporting upstream ? |
487 | self.addr = self.socket.getsockname() |
488 | @@ -155,13 +177,6 @@ |
489 | authorizer.add_anonymous(self._root, perm='elradfmwM') |
490 | self._ftp_server = ftp_server(address, BzrConformingFTPHandler, |
491 | authorizer) |
492 | - # This is hacky as hell, will not work if we need two servers working |
493 | - # at the same time, but that's the best we can do so far... |
494 | - # FIXME: At least log and logline could be overriden in the handler ? |
495 | - # -- vila 20090227 |
496 | - ftpserver.log = self.log |
497 | - ftpserver.logline = self.log |
498 | - ftpserver.logerror = self.log |
499 | |
500 | self._port = self._ftp_server.socket.getsockname()[1] |
501 | self._ftpd_starting = threading.Lock() |
502 | @@ -196,11 +211,11 @@ |
503 | self._ftpd_starting.release() |
504 | while self._ftpd_running: |
505 | try: |
506 | - self._ftp_server.serve_forever(timeout=0.1, count=1) |
507 | + self._ftp_server.serve_forever(timeout=0.1) |
508 | except select.error as e: |
509 | if e.args[0] != errno.EBADF: |
510 | raise |
511 | - self._ftp_server.close_all(ignore_all=True) |
512 | + self._ftp_server.close_all() |
513 | |
514 | def add_user(self, user, password): |
515 | """Add a user with write access.""" |
516 | |
517 | === modified file 'doc/en/release-notes/brz-3.0.txt' |
518 | --- doc/en/release-notes/brz-3.0.txt 2017-06-11 01:27:46 +0000 |
519 | +++ doc/en/release-notes/brz-3.0.txt 2017-06-11 13:38:25 +0000 |
520 | @@ -41,6 +41,9 @@ |
521 | URL schemes "http+pycurl://" and "https+pycurl://" has been dropped. |
522 | (Jelmer Vernooij, #82086, #377389, #122258, #516222, #545776, #1696602) |
523 | |
524 | + * Support for medusa for FTP tests has been dropped, only |
525 | + pyftpdlib is now supported. (Jelmer Vernooij) |
526 | + |
527 | New Features |
528 | ************ |
529 | |
530 | @@ -135,5 +138,8 @@ |
531 | which caused ``output_encoding = iso-8859-1`` to be added to the |
532 | users' bazaar.conf. (Jelmer Vernooij) |
533 | |
534 | + * Newer versions of ``pyftpdlib`` are now supported for running FTP tests. |
535 | + (Jelmer Vernooij) |
536 | + |
537 | .. |
538 | vim: tw=74 ft=rst ff=unix |
539 | |
540 | === modified file 'setup.py' |
541 | --- setup.py 2017-06-07 00:34:37 +0000 |
542 | +++ setup.py 2017-06-11 13:38:25 +0000 |
543 | @@ -587,7 +587,6 @@ |
544 | excludes = """Tkinter psyco ElementPath r_hmac |
545 | ImaginaryModule cElementTree elementtree.ElementTree |
546 | Crypto.PublicKey._fastmath |
547 | - medusa medusa.filesys medusa.ftp_server |
548 | tools |
549 | resource validate""".split() |
550 | dll_excludes = [] |
Looks good to me.