~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

Viewing changes to bzrlib/tests/stub_sftp.py

  • Committer: Canonical.com Patch Queue Manager
  • Date: 2010-07-16 14:02:58 UTC
  • mfrom: (5346.2.3 doc)
  • Revision ID: pqm@pqm.ubuntu.com-20100716140258-js1p8i24w8nodz6t
(mbp) developer docs about transports and symlinks (Martin Pool)

Show diffs side-by-side

added added

removed removed

Lines of Context:
23
23
import paramiko
24
24
import select
25
25
import socket
26
 
import SocketServer
27
26
import sys
28
27
import threading
29
28
import time
39
38
from bzrlib.tests import test_server
40
39
 
41
40
 
42
 
class StubServer(paramiko.ServerInterface):
 
41
class StubServer (paramiko.ServerInterface):
43
42
 
44
 
    def __init__(self, test_case_server):
 
43
    def __init__(self, test_case):
45
44
        paramiko.ServerInterface.__init__(self)
46
 
        self.log = test_case_server.log
 
45
        self._test_case = test_case
47
46
 
48
47
    def check_auth_password(self, username, password):
49
48
        # all are allowed
50
 
        self.log('sftpserver - authorizing: %s' % (username,))
 
49
        self._test_case.log('sftpserver - authorizing: %s' % (username,))
51
50
        return paramiko.AUTH_SUCCESSFUL
52
51
 
53
52
    def check_channel_request(self, kind, chanid):
54
 
        self.log('sftpserver - channel request: %s, %s' % (kind, chanid))
 
53
        self._test_case.log(
 
54
            'sftpserver - channel request: %s, %s' % (kind, chanid))
55
55
        return paramiko.OPEN_SUCCEEDED
56
56
 
57
57
 
58
 
class StubSFTPHandle(paramiko.SFTPHandle):
59
 
 
 
58
class StubSFTPHandle (paramiko.SFTPHandle):
60
59
    def stat(self):
61
60
        try:
62
61
            return paramiko.SFTPAttributes.from_stat(
74
73
            return paramiko.SFTPServer.convert_errno(e.errno)
75
74
 
76
75
 
77
 
class StubSFTPServer(paramiko.SFTPServerInterface):
 
76
class StubSFTPServer (paramiko.SFTPServerInterface):
78
77
 
79
78
    def __init__(self, server, root, home=None):
80
79
        paramiko.SFTPServerInterface.__init__(self, server)
91
90
            self.home = home[len(self.root):]
92
91
        if self.home.startswith('/'):
93
92
            self.home = self.home[1:]
94
 
        server.log('sftpserver - new connection')
 
93
        server._test_case.log('sftpserver - new connection')
95
94
 
96
95
    def _realpath(self, path):
97
96
        # paths returned from self.canonicalize() always start with
242
241
    # removed: chattr, symlink, readlink
243
242
    # (nothing in bzr's sftp transport uses those)
244
243
 
245
 
 
246
244
# ------------- server test implementation --------------
247
245
 
248
246
STUB_SERVER_KEY = """
264
262
"""
265
263
 
266
264
 
 
265
class SocketListener(threading.Thread):
 
266
 
 
267
    def __init__(self, callback):
 
268
        threading.Thread.__init__(self)
 
269
        self._callback = callback
 
270
        self._socket = socket.socket()
 
271
        self._socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
 
272
        self._socket.bind(('localhost', 0))
 
273
        self._socket.listen(1)
 
274
        self.host, self.port = self._socket.getsockname()[:2]
 
275
        self._stop_event = threading.Event()
 
276
 
 
277
    def stop(self):
 
278
        # called from outside this thread
 
279
        self._stop_event.set()
 
280
        # use a timeout here, because if the test fails, the server thread may
 
281
        # never notice the stop_event.
 
282
        self.join(5.0)
 
283
        self._socket.close()
 
284
 
 
285
    def run(self):
 
286
        trace.mutter('SocketListener %r has started', self)
 
287
        while True:
 
288
            readable, writable_unused, exception_unused = \
 
289
                select.select([self._socket], [], [], 0.1)
 
290
            if self._stop_event.isSet():
 
291
                trace.mutter('SocketListener %r has stopped', self)
 
292
                return
 
293
            if len(readable) == 0:
 
294
                continue
 
295
            try:
 
296
                s, addr_unused = self._socket.accept()
 
297
                trace.mutter('SocketListener %r has accepted connection %r',
 
298
                    self, s)
 
299
                # because the loopback socket is inline, and transports are
 
300
                # never explicitly closed, best to launch a new thread.
 
301
                threading.Thread(target=self._callback, args=(s,)).start()
 
302
            except socket.error, x:
 
303
                sys.excepthook(*sys.exc_info())
 
304
                trace.warning('Socket error during accept() '
 
305
                              'within unit test server thread: %r' % x)
 
306
            except Exception, x:
 
307
                # probably a failed test; unit test thread will log the
 
308
                # failure/error
 
309
                sys.excepthook(*sys.exc_info())
 
310
                trace.warning(
 
311
                    'Exception from within unit test server thread: %r' % x)
 
312
 
 
313
 
267
314
class SocketDelay(object):
268
315
    """A socket decorator to make TCP appear slower.
269
316
 
339
386
        return bytes_sent
340
387
 
341
388
 
342
 
class TestingSFTPConnectionHandler(SocketServer.BaseRequestHandler):
343
 
 
344
 
    def setup(self):
345
 
        self.wrap_for_latency()
346
 
        tcs = self.server.test_case_server
347
 
        ssh_server = paramiko.Transport(self.request)
348
 
        ssh_server.add_server_key(tcs.get_host_key())
349
 
        ssh_server.set_subsystem_handler('sftp', paramiko.SFTPServer,
350
 
                                         StubSFTPServer, root=tcs._root,
351
 
                                         home=tcs._server_homedir)
352
 
        server = tcs._server_interface(tcs)
353
 
        ssh_server.start_server(None, server)
354
 
        # FIXME: Long story short:
355
 
        # bt.test_transport.TestSSHConnections.test_bzr_connect_to_bzr_ssh
356
 
        # fails if we wait less than 0.2 seconds... paramiko uses a lot of
357
 
        # timeouts internally which probably mask a synchronisation
358
 
        # problem. Note that this is the only test that requires this hack and
359
 
        # the test may need to be fixed instead, but it's late and the test is
360
 
        # horrible as mentioned in its comments :) -- vila 20100623
361
 
        import time
362
 
        time.sleep(0.2)
363
 
 
364
 
    def wrap_for_latency(self):
365
 
        tcs = self.server.test_case_server
366
 
        if tcs.add_latency:
367
 
            # Give the socket (which the request really is) a latency adding
368
 
            # decorator.
369
 
            self.request = SocketDelay(self.request, tcs.add_latency)
370
 
 
371
 
 
372
 
class TestingSFTPWithoutSSHConnectionHandler(TestingSFTPConnectionHandler):
373
 
 
374
 
    def setup(self):
375
 
        self.wrap_for_latency()
376
 
        # Re-import these as locals, so that they're still accessible during
377
 
        # interpreter shutdown (when all module globals get set to None, leading
378
 
        # to confusing errors like "'NoneType' object has no attribute 'error'".
379
 
        class FakeChannel(object):
380
 
            def get_transport(self):
381
 
                return self
382
 
            def get_log_channel(self):
383
 
                return 'paramiko'
384
 
            def get_name(self):
385
 
                return '1'
386
 
            def get_hexdump(self):
387
 
                return False
388
 
            def close(self):
389
 
                pass
390
 
 
391
 
        tcs = self.server.test_case_server
392
 
        server = paramiko.SFTPServer(
393
 
            FakeChannel(), 'sftp', StubServer(tcs), StubSFTPServer,
394
 
            root=tcs._root, home=tcs._server_homedir)
395
 
        try:
396
 
            server.start_subsystem(
397
 
                'sftp', None, ssh.SocketAsChannelAdapter(self.request))
398
 
        except socket.error, e:
399
 
            if (len(e.args) > 0) and (e.args[0] == errno.EPIPE):
400
 
                # it's okay for the client to disconnect abruptly
401
 
                # (bug in paramiko 1.6: it should absorb this exception)
402
 
                pass
403
 
            else:
404
 
                raise
405
 
        except Exception, e:
406
 
            # This typically seems to happen during interpreter shutdown, so
407
 
            # most of the useful ways to report this error won't work.
408
 
            # Writing the exception type, and then the text of the exception,
409
 
            # seems to be the best we can do.
410
 
            # FIXME: All interpreter shutdown errors should have been related
411
 
            # to daemon threads, cleanup needed -- vila 20100623
412
 
            import sys
413
 
            sys.stderr.write('\nEXCEPTION %r: ' % (e.__class__,))
414
 
            sys.stderr.write('%s\n\n' % (e,))
415
 
        server.finish_subsystem()
416
 
 
417
 
 
418
 
class TestingSFTPServer(test_server.TestingThreadingTCPServer):
419
 
 
420
 
    def __init__(self, server_address, request_handler_class, test_case_server):
421
 
        test_server.TestingThreadingTCPServer.__init__(
422
 
            self, server_address, request_handler_class)
423
 
        self.test_case_server = test_case_server
424
 
 
425
 
 
426
 
class SFTPServer(test_server.TestingTCPServerInAThread):
 
389
class SFTPServer(test_server.TestServer):
427
390
    """Common code for SFTP server facilities."""
428
391
 
429
392
    def __init__(self, server_interface=StubServer):
430
 
        self.host = '127.0.0.1'
431
 
        self.port = 0
432
 
        super(SFTPServer, self).__init__((self.host, self.port),
433
 
                                         TestingSFTPServer,
434
 
                                         TestingSFTPConnectionHandler)
435
393
        self._original_vendor = None
 
394
        self._homedir = None
 
395
        self._server_homedir = None
 
396
        self._listener = None
 
397
        self._root = None
436
398
        self._vendor = ssh.ParamikoVendor()
437
399
        self._server_interface = server_interface
438
 
        self._host_key = None
 
400
        # sftp server logs
439
401
        self.logs = []
440
402
        self.add_latency = 0
441
 
        self._homedir = None
442
 
        self._server_homedir = None
443
 
        self._root = None
444
403
 
445
404
    def _get_sftp_url(self, path):
446
405
        """Calculate an sftp url to this server for path."""
447
 
        return "sftp://foo:bar@%s:%s/%s" % (self.host, self.port, path)
 
406
        return 'sftp://foo:bar@%s:%d/%s' % (self._listener.host,
 
407
                                            self._listener.port, path)
448
408
 
449
409
    def log(self, message):
450
410
        """StubServer uses this to log when a new server is created."""
451
411
        self.logs.append(message)
452
412
 
453
 
    def create_server(self):
454
 
        server = self.server_class((self.host, self.port),
455
 
                                   self.request_handler_class,
456
 
                                   self)
457
 
        return server
458
 
 
459
 
    def get_host_key(self):
460
 
        if self._host_key is None:
461
 
            key_file = osutils.pathjoin(self._homedir, 'test_rsa.key')
462
 
            f = open(key_file, 'w')
463
 
            try:
464
 
                f.write(STUB_SERVER_KEY)
465
 
            finally:
466
 
                f.close()
467
 
            self._host_key = paramiko.RSAKey.from_private_key_file(key_file)
468
 
        return self._host_key
 
413
    def _run_server_entry(self, sock):
 
414
        """Entry point for all implementations of _run_server.
 
415
 
 
416
        If self.add_latency is > 0.000001 then sock is given a latency adding
 
417
        decorator.
 
418
        """
 
419
        if self.add_latency > 0.000001:
 
420
            sock = SocketDelay(sock, self.add_latency)
 
421
        return self._run_server(sock)
 
422
 
 
423
    def _run_server(self, s):
 
424
        ssh_server = paramiko.Transport(s)
 
425
        key_file = osutils.pathjoin(self._homedir, 'test_rsa.key')
 
426
        f = open(key_file, 'w')
 
427
        f.write(STUB_SERVER_KEY)
 
428
        f.close()
 
429
        host_key = paramiko.RSAKey.from_private_key_file(key_file)
 
430
        ssh_server.add_server_key(host_key)
 
431
        server = self._server_interface(self)
 
432
        ssh_server.set_subsystem_handler('sftp', paramiko.SFTPServer,
 
433
                                         StubSFTPServer, root=self._root,
 
434
                                         home=self._server_homedir)
 
435
        event = threading.Event()
 
436
        ssh_server.start_server(event, server)
 
437
        event.wait(5.0)
469
438
 
470
439
    def start_server(self, backing_server=None):
471
440
        # XXX: TODO: make sftpserver back onto backing_server rather than local
490
459
        self._root = '/'
491
460
        if sys.platform == 'win32':
492
461
            self._root = ''
493
 
        super(SFTPServer, self).start_server()
 
462
        self._listener = SocketListener(self._run_server_entry)
 
463
        self._listener.setDaemon(True)
 
464
        self._listener.start()
494
465
 
495
466
    def stop_server(self):
496
 
        try:
497
 
            super(SFTPServer, self).stop_server()
498
 
        finally:
499
 
            ssh._ssh_vendor_manager._cached_ssh_vendor = self._original_vendor
 
467
        self._listener.stop()
 
468
        ssh._ssh_vendor_manager._cached_ssh_vendor = self._original_vendor
500
469
 
501
470
    def get_bogus_url(self):
502
471
        """See bzrlib.transport.Server.get_bogus_url."""
503
 
        # this is chosen to try to prevent trouble with proxies, weird dns, etc
 
472
        # this is chosen to try to prevent trouble with proxies, wierd dns, etc
504
473
        # we bind a random socket, so that we get a guaranteed unused port
505
474
        # we just never listen on that port
506
475
        s = socket.socket()
526
495
    def __init__(self):
527
496
        super(SFTPServerWithoutSSH, self).__init__()
528
497
        self._vendor = ssh.LoopbackVendor()
529
 
        self.request_handler_class = TestingSFTPWithoutSSHConnectionHandler
530
 
 
531
 
    def get_host_key():
532
 
        return None
 
498
 
 
499
    def _run_server(self, sock):
 
500
        # Re-import these as locals, so that they're still accessible during
 
501
        # interpreter shutdown (when all module globals get set to None, leading
 
502
        # to confusing errors like "'NoneType' object has no attribute 'error'".
 
503
        class FakeChannel(object):
 
504
            def get_transport(self):
 
505
                return self
 
506
            def get_log_channel(self):
 
507
                return 'paramiko'
 
508
            def get_name(self):
 
509
                return '1'
 
510
            def get_hexdump(self):
 
511
                return False
 
512
            def close(self):
 
513
                pass
 
514
 
 
515
        server = paramiko.SFTPServer(
 
516
            FakeChannel(), 'sftp', StubServer(self), StubSFTPServer,
 
517
            root=self._root, home=self._server_homedir)
 
518
        try:
 
519
            server.start_subsystem(
 
520
                'sftp', None, ssh.SocketAsChannelAdapter(sock))
 
521
        except socket.error, e:
 
522
            if (len(e.args) > 0) and (e.args[0] == errno.EPIPE):
 
523
                # it's okay for the client to disconnect abruptly
 
524
                # (bug in paramiko 1.6: it should absorb this exception)
 
525
                pass
 
526
            else:
 
527
                raise
 
528
        except Exception, e:
 
529
            # This typically seems to happen during interpreter shutdown, so
 
530
            # most of the useful ways to report this error are won't work.
 
531
            # Writing the exception type, and then the text of the exception,
 
532
            # seems to be the best we can do.
 
533
            import sys
 
534
            sys.stderr.write('\nEXCEPTION %r: ' % (e.__class__,))
 
535
            sys.stderr.write('%s\n\n' % (e,))
 
536
        server.finish_subsystem()
533
537
 
534
538
 
535
539
class SFTPAbsoluteServer(SFTPServerWithoutSSH):
558
562
    It does this by serving from a deeply-nested directory that doesn't exist.
559
563
    """
560
564
 
561
 
    def create_server(self):
562
 
        # FIXME: Can't we do that in a cleaner way ? -- vila 20100623
563
 
        server = super(SFTPSiblingAbsoluteServer, self).create_server()
564
 
        server._server_homedir = '/dev/noone/runs/tests/here'
565
 
        return server
 
565
    def start_server(self, backing_server=None):
 
566
        self._server_homedir = '/dev/noone/runs/tests/here'
 
567
        super(SFTPSiblingAbsoluteServer, self).start_server(backing_server)
566
568