← Back to team overview

launchpad-reviewers team mailing list archive

[Merge] lp:~cjwatson/launchpad/split-lazr.sshserver into lp:launchpad

 

Colin Watson has proposed merging lp:~cjwatson/launchpad/split-lazr.sshserver into lp:launchpad.

Commit message:
Split out lp.services.sshserver to lazr.sshserver.

Requested reviews:
  Launchpad code reviewers (launchpad-reviewers)

For more details, see:
https://code.launchpad.net/~cjwatson/launchpad/split-lazr.sshserver/+merge/245647

Split out lp.services.sshserver to lazr.sshserver.

(lazr.sshserver isn't in lp-source-dependencies yet, because we first want to make sure that poppy can be split out and use it; but I'm proposing this merge in advance of that anyway to allow parallelising review.)
-- 
Your team Launchpad code reviewers is requested to review the proposed merge of lp:~cjwatson/launchpad/split-lazr.sshserver into lp:launchpad.
=== modified file 'daemons/poppy-sftp.tac'
--- daemons/poppy-sftp.tac	2012-03-26 05:50:20 +0000
+++ daemons/poppy-sftp.tac	2015-01-06 12:50:32 +0000
@@ -7,6 +7,13 @@
 
 import logging
 
+from lazr.sshserver.auth import (
+    LaunchpadAvatar,
+    PublicKeyFromLaunchpadChecker,
+    )
+from lazr.sshserver.service import SSHService
+from lazr.sshserver.session import DoNothingSession
+
 from twisted.application import service
 from twisted.conch.interfaces import ISession
 from twisted.conch.ssh import filetransfer
@@ -26,10 +33,6 @@
     FTPServiceFactory,
     )
 from lp.poppy.twistedsftp import SFTPServer
-from lp.services.sshserver.auth import (
-    LaunchpadAvatar, PublicKeyFromLaunchpadChecker)
-from lp.services.sshserver.service import SSHService
-from lp.services.sshserver.session import DoNothingSession
 from lp.services.twistedsupport.loggingsupport import set_up_oops_reporting
 
 

=== modified file 'daemons/sftp.tac'
--- daemons/sftp.tac	2011-12-29 05:29:36 +0000
+++ daemons/sftp.tac	2015-01-06 12:50:32 +0000
@@ -5,6 +5,7 @@
 #     twistd -noy sftp.tac
 # or similar.  Refer to the twistd(1) man page for details.
 
+from lazr.sshserver.service import SSHService
 from twisted.application import service
 from twisted.protocols.policies import TimeoutFactory
 
@@ -20,7 +21,6 @@
     PRIVATE_KEY_FILE,
     PUBLIC_KEY_FILE,
     )
-from lp.services.sshserver.service import SSHService
 from lp.services.twistedsupport.gracefulshutdown import (
     ConnTrackingFactoryWrapper,
     make_web_status_service,

=== modified file 'lib/lp/codehosting/sftp.py'
--- lib/lp/codehosting/sftp.py	2012-06-29 08:40:05 +0000
+++ lib/lp/codehosting/sftp.py	2015-01-06 12:50:32 +0000
@@ -30,6 +30,7 @@
     urlutils,
     )
 from bzrlib.transport.local import LocalTransport
+from lazr.sshserver.sftp import FileIsADirectory
 from twisted.conch.interfaces import (
     ISFTPFile,
     ISFTPServer,
@@ -45,7 +46,6 @@
     LaunchpadServer,
     )
 from lp.services.config import config
-from lp.services.sshserver.sftp import FileIsADirectory
 from lp.services.twistedsupport import gatherResults
 
 

=== modified file 'lib/lp/codehosting/sshserver/daemon.py'
--- lib/lp/codehosting/sshserver/daemon.py	2012-01-01 02:58:52 +0000
+++ lib/lp/codehosting/sshserver/daemon.py	2015-01-06 12:50:32 +0000
@@ -17,6 +17,10 @@
 
 import os
 
+from lazr.sshserver.auth import (
+    LaunchpadAvatar,
+    PublicKeyFromLaunchpadChecker,
+    )
 from twisted.conch.interfaces import ISession
 from twisted.conch.ssh import filetransfer
 from twisted.cred.portal import (
@@ -30,10 +34,6 @@
 from lp.codehosting import sftp
 from lp.codehosting.sshserver.session import launch_smart_server
 from lp.services.config import config
-from lp.services.sshserver.auth import (
-    LaunchpadAvatar,
-    PublicKeyFromLaunchpadChecker,
-    )
 
 # The names of the key files of the server itself. The directory itself is
 # given in config.codehosting.host_key_pair_path.

=== modified file 'lib/lp/codehosting/sshserver/session.py'
--- lib/lp/codehosting/sshserver/session.py	2012-06-29 08:40:05 +0000
+++ lib/lp/codehosting/sshserver/session.py	2015-01-06 12:50:32 +0000
@@ -14,6 +14,8 @@
 import sys
 import urlparse
 
+from lazr.sshserver.events import AvatarEvent
+from lazr.sshserver.session import DoNothingSession
 from twisted.internet import (
     error,
     interfaces,
@@ -25,8 +27,6 @@
 
 from lp.codehosting import get_bzr_path
 from lp.services.config import config
-from lp.services.sshserver.events import AvatarEvent
-from lp.services.sshserver.session import DoNothingSession
 
 
 class BazaarSSHStarted(AvatarEvent):

=== modified file 'lib/lp/codehosting/sshserver/tests/test_daemon.py'
--- lib/lp/codehosting/sshserver/tests/test_daemon.py	2010-10-30 22:44:21 +0000
+++ lib/lp/codehosting/sshserver/tests/test_daemon.py	2015-01-06 12:50:32 +0000
@@ -5,6 +5,11 @@
 
 __metaclass__ = type
 
+from lazr.sshserver.auth import (
+    NoSuchPersonWithName,
+    SSHUserAuthServer,
+    )
+from lazr.sshserver.service import Factory
 from twisted.conch.ssh.common import NS
 from twisted.conch.ssh.keys import Key
 from twisted.test.proto_helpers import StringTransport
@@ -15,9 +20,8 @@
     PRIVATE_KEY_FILE,
     PUBLIC_KEY_FILE,
     )
-from lp.services.sshserver.auth import SSHUserAuthServer
-from lp.services.sshserver.service import Factory
 from lp.testing import TestCase
+from lp.xmlrpc import faults
 
 
 class StringTransportWith_setTcpKeepAlive(StringTransport):
@@ -85,3 +89,14 @@
         mind2 = server_transport2.service.getMind()
 
         self.assertIsNot(mind1.cache, mind2.cache)
+
+
+class TestXMLRPC(TestCase):
+    """Test XML-RPC protocol integrity."""
+
+    def test_NoSuchPersonWithName_error_code(self):
+        # The error code for NoSuchPersonWithName in lazr.sshserver matches
+        # that in lp.xmlrpc.faults.
+        self.assertEqual(
+            faults.NoSuchPersonWithName.error_code,
+            NoSuchPersonWithName.error_code)

=== modified file 'lib/lp/codehosting/tests/test_sftp.py'
--- lib/lp/codehosting/tests/test_sftp.py	2011-12-22 09:05:46 +0000
+++ lib/lp/codehosting/tests/test_sftp.py	2015-01-06 12:50:32 +0000
@@ -13,6 +13,7 @@
 from bzrlib.tests import TestCaseInTempDir
 from bzrlib.transport import get_transport
 from bzrlib.transport.memory import MemoryTransport
+from lazr.sshserver.sftp import FileIsADirectory
 from testtools.deferredruntest import (
     assert_fails_with,
     AsynchronousDeferredRunTest,
@@ -33,7 +34,6 @@
     TransportSFTPServer,
     )
 from lp.codehosting.sshserver.daemon import CodehostingAvatar
-from lp.services.sshserver.sftp import FileIsADirectory
 from lp.services.utils import file_exists
 from lp.testing import TestCase
 from lp.testing.factory import LaunchpadObjectFactory

=== modified file 'lib/lp/poppy/tests/test_twistedsftp.py'
--- lib/lp/poppy/tests/test_twistedsftp.py	2011-11-30 16:27:15 +0000
+++ lib/lp/poppy/tests/test_twistedsftp.py	2015-01-06 12:50:32 +0000
@@ -8,9 +8,9 @@
 import os
 
 from fixtures import TempDir
+from lazr.sshserver.sftp import FileIsADirectory
 
 from lp.poppy.twistedsftp import SFTPServer
-from lp.services.sshserver.sftp import FileIsADirectory
 from lp.testing import (
     NestedTempfile,
     TestCase,

=== modified file 'lib/lp/poppy/twistedsftp.py'
--- lib/lp/poppy/twistedsftp.py	2012-06-29 08:40:05 +0000
+++ lib/lp/poppy/twistedsftp.py	2015-01-06 12:50:32 +0000
@@ -14,6 +14,8 @@
 import os
 import tempfile
 
+from lazr.sshserver.events import SFTPClosed
+from lazr.sshserver.sftp import FileIsADirectory
 from twisted.conch.interfaces import (
     ISFTPFile,
     ISFTPServer,
@@ -26,8 +28,6 @@
 
 from lp.poppy.filesystem import UploadFileSystem
 from lp.poppy.hooks import Hooks
-from lp.services.sshserver.events import SFTPClosed
-from lp.services.sshserver.sftp import FileIsADirectory
 
 
 class SFTPServer:

=== removed directory 'lib/lp/services/sshserver'
=== removed file 'lib/lp/services/sshserver/__init__.py'
--- lib/lp/services/sshserver/__init__.py	2010-04-15 14:49:55 +0000
+++ lib/lp/services/sshserver/__init__.py	1970-01-01 00:00:00 +0000
@@ -1,8 +0,0 @@
-# Copyright 2010 Canonical Ltd.  This software is licensed under the
-# GNU Affero General Public License version 3 (see the file LICENSE).
-
-"""The Launchpad SSH server."""
-
-__metaclass__ = type
-__all__ = []
-

=== removed file 'lib/lp/services/sshserver/accesslog.py'
--- lib/lp/services/sshserver/accesslog.py	2014-08-29 04:29:16 +0000
+++ lib/lp/services/sshserver/accesslog.py	1970-01-01 00:00:00 +0000
@@ -1,86 +0,0 @@
-# Copyright 2009 Canonical Ltd.  This software is licensed under the
-# GNU Affero General Public License version 3 (see the file LICENSE).
-
-"""Logging for the SSH server."""
-
-__metaclass__ = type
-__all__ = [
-    'LoggingManager',
-    ]
-
-import logging
-from logging.handlers import WatchedFileHandler
-
-from twisted.python import log as tplog
-from zope.component import (
-    adapter,
-    getGlobalSiteManager,
-    provideHandler,
-    )
-# This non-standard import is necessary to hook up the event system.
-import zope.component.event
-
-from lp.services.sshserver.events import ILoggingEvent
-from lp.services.utils import synchronize
-
-
-class LoggingManager:
-    """Class for managing SSH server logging."""
-
-    def __init__(self, main_log, access_log, access_log_path):
-        """Construct the logging manager.
-
-        :param main_log: The main log. Twisted will log to this.
-        :param access_log: The access log object.
-        :param access_log_path: The path to the file where access log
-            messages go.
-        """
-        self._main_log = main_log
-        self._access_log = access_log
-        self._access_log_path = access_log_path
-        self._is_set_up = False
-
-    def setUp(self):
-        """Set up logging for the smart server.
-
-        This sets up a debugging handler on the main logger and makes sure
-        that things logged there won't go to stderr. It also sets up an access
-        logger.
-        """
-        log = self._main_log
-        self._orig_level = log.level
-        self._orig_handlers = list(log.handlers)
-        self._orig_observers = list(tplog.theLogPublisher.observers)
-        log.setLevel(logging.INFO)
-        log.addHandler(logging.NullHandler())
-        handler = WatchedFileHandler(self._access_log_path)
-        handler.setFormatter(
-            logging.Formatter("%(asctime)s %(levelname)s %(message)s"))
-        self._access_log.addHandler(handler)
-        self._access_log.setLevel(logging.INFO)
-        # Make sure that our logging event handler is there, ready to receive
-        # logging events.
-        provideHandler(self._log_event)
-        self._is_set_up = True
-
-    @adapter(ILoggingEvent)
-    def _log_event(self, event):
-        """Log 'event' to the access log."""
-        self._access_log.log(event.level, event.message)
-
-    def tearDown(self):
-        if not self._is_set_up:
-            return
-        log = self._main_log
-        log.level = self._orig_level
-        synchronize(
-            log.handlers, self._orig_handlers, log.addHandler,
-            log.removeHandler)
-        synchronize(
-            self._access_log.handlers, self._orig_handlers,
-            self._access_log.addHandler, self._access_log.removeHandler)
-        synchronize(
-            tplog.theLogPublisher.observers, self._orig_observers,
-            tplog.addObserver, tplog.removeObserver)
-        getGlobalSiteManager().unregisterHandler(self._log_event)
-        self._is_set_up = False

=== removed file 'lib/lp/services/sshserver/auth.py'
--- lib/lp/services/sshserver/auth.py	2014-01-30 15:04:06 +0000
+++ lib/lp/services/sshserver/auth.py	1970-01-01 00:00:00 +0000
@@ -1,308 +0,0 @@
-# Copyright 2009 Canonical Ltd.  This software is licensed under the
-# GNU Affero General Public License version 3 (see the file LICENSE).
-
-"""Custom authentication for the SSH server.
-
-Launchpad's SSH server authenticates users against a XML-RPC service (see
-`lp.services.authserver.interfaces.IAuthServer` and
-`PublicKeyFromLaunchpadChecker`) and provides richer error messages in the
-case of failed authentication (see `SSHUserAuthServer`).
-"""
-
-__metaclass__ = type
-__all__ = [
-    'LaunchpadAvatar',
-    'PublicKeyFromLaunchpadChecker',
-    'SSHUserAuthServer',
-    ]
-
-import binascii
-
-from twisted.conch import avatar
-from twisted.conch.checkers import SSHPublicKeyDatabase
-from twisted.conch.error import ConchError
-from twisted.conch.interfaces import IConchUser
-from twisted.conch.ssh import (
-    keys,
-    userauth,
-    )
-from twisted.conch.ssh.common import (
-    getNS,
-    NS,
-    )
-from twisted.cred import credentials
-from twisted.cred.checkers import ICredentialsChecker
-from twisted.cred.error import UnauthorizedLogin
-from twisted.internet import defer
-from twisted.python import failure
-from zope.event import notify
-from zope.interface import implements
-
-from lp.services.sshserver import events
-from lp.services.sshserver.session import PatchedSSHSession
-from lp.services.sshserver.sftp import FileTransferServer
-from lp.services.twistedsupport.xmlrpc import trap_fault
-from lp.xmlrpc import faults
-
-
-class LaunchpadAvatar(avatar.ConchUser):
-    """An account on the SSH server, corresponding to a Launchpad person.
-
-    :ivar channelLookup: See `avatar.ConchUser`.
-    :ivar subsystemLookup: See `avatar.ConchUser`.
-    :ivar user_id: The Launchpad database ID of the Person for this account.
-    :ivar username: The Launchpad username for this account.
-    """
-
-    def __init__(self, user_dict):
-        """Construct a `LaunchpadAvatar`.
-
-        :param user_dict: The result of a call to
-            `IAuthServer.getUserAndSSHKeys`.
-        """
-        avatar.ConchUser.__init__(self)
-        self.user_id = user_dict['id']
-        self.username = user_dict['name']
-
-        # Set the only channel as a standard SSH session (with a couple of bug
-        # fixes).
-        self.channelLookup = {'session': PatchedSSHSession}
-        # ...and set the only subsystem to be SFTP.
-        self.subsystemLookup = {'sftp': FileTransferServer}
-
-    def logout(self):
-        notify(events.UserLoggedOut(self))
-
-
-class UserDisplayedUnauthorizedLogin(UnauthorizedLogin):
-    """UnauthorizedLogin which should be reported to the user."""
-
-
-class ISSHPrivateKeyWithMind(credentials.ISSHPrivateKey):
-    """Marker interface for SSH credentials that reference a Mind."""
-
-
-class SSHPrivateKeyWithMind(credentials.SSHPrivateKey):
-    """SSH credentials that also reference a Mind."""
-
-    implements(ISSHPrivateKeyWithMind)
-
-    def __init__(self, username, algName, blob, sigData, signature, mind):
-        credentials.SSHPrivateKey.__init__(
-            self, username, algName, blob, sigData, signature)
-        self.mind = mind
-
-
-class UserDetailsMind:
-    """A 'Mind' object that answers and caches requests for user details.
-
-    A mind is a (poorly named) concept from twisted.cred that basically can be
-    passed to portal.login to represent the client side view of
-    authentication.  In our case we attach a mind to the SSHUserAuthServer
-    object that corresponds to an attempt to authenticate against the server.
-    """
-
-    def __init__(self):
-        self.cache = {}
-
-    def lookupUserDetails(self, proxy, username):
-        """Find details for the named user, including registered SSH keys.
-
-        This method basically wraps `IAuthServer.getUserAndSSHKeys` -- see the
-        documentation of that method for more details -- and caches the
-        details found for any particular user.
-
-        :param proxy: A twisted.web.xmlrpc.Proxy object for the authentication
-            endpoint.
-        :param username: The username to look up.
-        """
-        if username in self.cache:
-            return defer.succeed(self.cache[username])
-        else:
-            d = proxy.callRemote('getUserAndSSHKeys', username)
-            d.addBoth(self._add_to_cache, username)
-            return d
-
-    def _add_to_cache(self, result, username):
-        """Add the results to our cache."""
-        self.cache[username] = result
-        return result
-
-
-class SSHUserAuthServer(userauth.SSHUserAuthServer):
-    """Subclass of Conch's SSHUserAuthServer to customize various behaviours.
-
-    There are two main differences:
-
-     * We override ssh_USERAUTH_REQUEST to display as a banner the reason why
-       an authentication attempt failed.
-
-     * We override auth_publickey to create credentials that reference a
-       UserDetailsMind and pass the same mind to self.portal.login.
-
-    Conch is not written in a way to make this easy; we've had to copy and
-    paste and change the implementations of these methods.
-    """
-
-    def __init__(self, transport=None, banner=None):
-        self.transport = transport
-        self._banner = banner
-        self._configured_banner_sent = False
-        self._mind = UserDetailsMind()
-        self.interfaceToMethod = userauth.SSHUserAuthServer.interfaceToMethod
-        self.interfaceToMethod[ISSHPrivateKeyWithMind] = 'publickey'
-
-    def sendBanner(self, text, language='en'):
-        bytes = '\r\n'.join(text.encode('UTF8').splitlines() + [''])
-        self.transport.sendPacket(userauth.MSG_USERAUTH_BANNER,
-                                  NS(bytes) + NS(language))
-
-    def _sendConfiguredBanner(self, passed_through):
-        if not self._configured_banner_sent and self._banner:
-            self._configured_banner_sent = True
-            self.sendBanner(self._banner)
-        return passed_through
-
-    def ssh_USERAUTH_REQUEST(self, packet):
-        # This is copied and pasted from twisted/conch/ssh/userauth.py in
-        # Twisted 8.0.1. We do this so we can add _ebLogToBanner between
-        # two existing errbacks.
-        user, nextService, method, rest = getNS(packet, 3)
-        if user != self.user or nextService != self.nextService:
-            self.authenticatedWith = [] # clear auth state
-        self.user = user
-        self.nextService = nextService
-        self.method = method
-        d = self.tryAuth(method, user, rest)
-        if not d:
-            self._ebBadAuth(failure.Failure(ConchError('auth returned none')))
-            return
-        d.addCallback(self._sendConfiguredBanner)
-        d.addCallbacks(self._cbFinishedAuth)
-        d.addErrback(self._ebMaybeBadAuth)
-        # This line does not appear in the original.
-        d.addErrback(self._ebLogToBanner)
-        d.addErrback(self._ebBadAuth)
-        return d
-
-    def _cbFinishedAuth(self, result):
-        ret = userauth.SSHUserAuthServer._cbFinishedAuth(self, result)
-        # Tell the avatar about the transport, so we can tie it to the
-        # connection in the logs.
-        avatar = self.transport.avatar
-        avatar.transport = self.transport
-        notify(events.UserLoggedIn(avatar))
-        return ret
-
-    def _ebLogToBanner(self, reason):
-        reason.trap(UserDisplayedUnauthorizedLogin)
-        self.sendBanner(reason.getErrorMessage())
-        return reason
-
-    def getMind(self):
-        """Return the mind that should be passed to self.portal.login().
-
-        If multiple requests to authenticate within this overall login attempt
-        should share state, this method can return the same mind each time.
-        """
-        return self._mind
-
-    def makePublicKeyCredentials(self, username, algName, blob, sigData,
-                                 signature):
-        """Construct credentials for a request to login with a public key.
-
-        Our implementation returns a SSHPrivateKeyWithMind.
-
-        :param username: The username the request is for.
-        :param algName: The algorithm name for the blob.
-        :param blob: The public key blob as sent by the client.
-        :param sigData: The data the signature was made from.
-        :param signature: The signed data.  This is checked to verify that the
-            user owns the private key.
-        """
-        mind = self.getMind()
-        return SSHPrivateKeyWithMind(
-                username, algName, blob, sigData, signature, mind)
-
-    def auth_publickey(self, packet):
-        # This is copied and pasted from twisted/conch/ssh/userauth.py in
-        # Twisted 8.0.1. We do this so we can customize how the credentials
-        # are built and pass a mind to self.portal.login.
-        hasSig = ord(packet[0])
-        algName, blob, rest = getNS(packet[1:], 2)
-        pubKey = keys.Key.fromString(blob).keyObject
-        signature = hasSig and getNS(rest)[0] or None
-        if hasSig:
-            b = NS(self.transport.sessionID) + \
-                chr(userauth.MSG_USERAUTH_REQUEST) +  NS(self.user) + \
-                NS(self.nextService) + NS('publickey') +  chr(hasSig) + \
-                NS(keys.objectType(pubKey)) + NS(blob)
-            # The next three lines are different from the original.
-            c = self.makePublicKeyCredentials(
-                self.user, algName, blob, b, signature)
-            return self.portal.login(c, self.getMind(), IConchUser)
-        else:
-            # The next four lines are different from the original.
-            c = self.makePublicKeyCredentials(
-                self.user, algName, blob, None, None)
-            return self.portal.login(
-                c, self.getMind(), IConchUser).addErrback(
-                    self._ebCheckKey, packet[1:])
-
-
-class PublicKeyFromLaunchpadChecker(SSHPublicKeyDatabase):
-    """Cred checker for getting public keys from launchpad.
-
-    It knows how to get the public keys from the authserver.
-    """
-    credentialInterfaces = ISSHPrivateKeyWithMind,
-    implements(ICredentialsChecker)
-
-    def __init__(self, authserver):
-        self.authserver = authserver
-
-    def checkKey(self, credentials):
-        """Check whether `credentials` is a valid request to authenticate.
-
-        We check the key data in credentials against the keys the named user
-        has registered in Launchpad.
-        """
-        d = credentials.mind.lookupUserDetails(
-            self.authserver, credentials.username)
-        d.addCallback(self._checkForAuthorizedKey, credentials)
-        d.addErrback(self._reportNoSuchUser, credentials)
-        return d
-
-    def _reportNoSuchUser(self, failure, credentials):
-        """Report the user named in the credentials not existing nicely."""
-        trap_fault(failure, faults.NoSuchPersonWithName)
-        raise UserDisplayedUnauthorizedLogin(
-            "No such Launchpad account: %s" % credentials.username)
-
-    def _checkForAuthorizedKey(self, user_dict, credentials):
-        """Check the key data in credentials against the keys found in LP."""
-        if credentials.algName == 'ssh-dss':
-            wantKeyType = 'DSA'
-        elif credentials.algName == 'ssh-rsa':
-            wantKeyType = 'RSA'
-        else:
-            # unknown key type
-            return False
-
-        if len(user_dict['keys']) == 0:
-            raise UserDisplayedUnauthorizedLogin(
-                "Launchpad user %r doesn't have a registered SSH key"
-                % credentials.username)
-
-        for keytype, keytext in user_dict['keys']:
-            if keytype != wantKeyType:
-                continue
-            try:
-                if keytext.decode('base64') == credentials.blob:
-                    return True
-            except binascii.Error:
-                continue
-
-        raise UnauthorizedLogin(
-            "Your SSH key does not match any key registered for Launchpad "
-            "user %s" % credentials.username)

=== removed file 'lib/lp/services/sshserver/events.py'
--- lib/lp/services/sshserver/events.py	2010-08-20 20:31:18 +0000
+++ lib/lp/services/sshserver/events.py	1970-01-01 00:00:00 +0000
@@ -1,148 +0,0 @@
-# Copyright 2010 Canonical Ltd.  This software is licensed under the
-# GNU Affero General Public License version 3 (see the file LICENSE).
-
-"""Events generated by the SSH server."""
-
-__metaclass__ = type
-__all__ = [
-    'AuthenticationFailed',
-    'AvatarEvent',
-    'ILoggingEvent',
-    'LoggingEvent',
-    'ServerStarting',
-    'ServerStopped',
-    'SFTPClosed',
-    'SFTPStarted',
-    'UserConnected',
-    'UserDisconnected',
-    'UserLoggedIn',
-    'UserLoggedOut',
-    ]
-
-import logging
-
-from zope.interface import (
-    Attribute,
-    implements,
-    Interface,
-    )
-
-
-class ILoggingEvent(Interface):
-    """An event is a logging event if it has a message and a severity level.
-
-    Events that provide this interface will be logged in the SSH server access
-    log.
-    """
-
-    level = Attribute("The level to log the event at.")
-    message = Attribute("The message to log.")
-
-
-class LoggingEvent:
-    """An event that can be logged to a Python logger.
-
-    :ivar level: The level to log itself as. This should be defined as a
-        class variable in subclasses.
-    :ivar template: The format string of the message to log. This should be
-        defined as a class variable in subclasses.
-    """
-
-    implements(ILoggingEvent)
-
-    def __init__(self, level=None, template=None, **data):
-        """Construct a logging event.
-
-        :param level: The level to log the event as. If specified, overrides
-            the 'level' class variable.
-        :param template: The format string of the message to log. If
-            specified, overrides the 'template' class variable.
-        :param **data: Information to be logged. Entries will be substituted
-            into the template and stored as attributes.
-        """
-        if level is not None:
-            self._level = level
-        if template is not None:
-            self.template = template
-        self._data = data
-
-    @property
-    def level(self):
-        """See `ILoggingEvent`."""
-        return self._level
-
-    @property
-    def message(self):
-        """See `ILoggingEvent`."""
-        return self.template % self._data
-
-
-class ServerStarting(LoggingEvent):
-
-    level = logging.INFO
-    template = '---- Server started ----'
-
-
-class ServerStopped(LoggingEvent):
-
-    level = logging.INFO
-    template = '---- Server stopped ----'
-
-
-class UserConnected(LoggingEvent):
-
-    level = logging.INFO
-    template = '[%(session_id)s] %(address)s connected.'
-
-    def __init__(self, transport, address):
-        LoggingEvent.__init__(
-            self, session_id=id(transport), address=address)
-
-
-class AuthenticationFailed(LoggingEvent):
-
-    level = logging.INFO
-    template = '[%(session_id)s] failed to authenticate.'
-
-    def __init__(self, transport):
-        LoggingEvent.__init__(self, session_id=id(transport))
-
-
-class UserDisconnected(LoggingEvent):
-
-    level = logging.INFO
-    template = '[%(session_id)s] disconnected.'
-
-    def __init__(self, transport):
-        LoggingEvent.__init__(self, session_id=id(transport))
-
-
-class AvatarEvent(LoggingEvent):
-    """Base avatar event."""
-
-    level = logging.INFO
-
-    def __init__(self, avatar):
-        self.avatar = avatar
-        LoggingEvent.__init__(
-            self, session_id=id(avatar.transport), username=avatar.username)
-
-
-class UserLoggedIn(AvatarEvent):
-
-    template = '[%(session_id)s] %(username)s logged in.'
-
-
-class UserLoggedOut(AvatarEvent):
-
-    template = '[%(session_id)s] %(username)s disconnected.'
-
-
-class SFTPStarted(AvatarEvent):
-
-    template = '[%(session_id)s] %(username)s started SFTP session.'
-
-
-class SFTPClosed(AvatarEvent):
-
-    template = '[%(session_id)s] %(username)s closed SFTP session.'

=== removed file 'lib/lp/services/sshserver/service.py'
--- lib/lp/services/sshserver/service.py	2012-04-16 23:02:44 +0000
+++ lib/lp/services/sshserver/service.py	1970-01-01 00:00:00 +0000
@@ -1,191 +0,0 @@
-# Copyright 2009-2011 Canonical Ltd.  This software is licensed under the
-# GNU Affero General Public License version 3 (see the file LICENSE).
-
-"""Twisted `service.Service` class for the Launchpad SSH server.
-
-An `SSHService` object can be used to launch the SSH server.
-"""
-
-__metaclass__ = type
-__all__ = [
-    'SSHService',
-    ]
-
-
-import logging
-import os
-
-from twisted.application import (
-    service,
-    strports,
-    )
-from twisted.conch.ssh.factory import SSHFactory
-from twisted.conch.ssh.keys import Key
-from twisted.conch.ssh.transport import SSHServerTransport
-from twisted.internet import defer
-from zope.event import notify
-
-from lp.services.sshserver import (
-    accesslog,
-    events,
-    )
-from lp.services.sshserver.auth import SSHUserAuthServer
-from lp.services.twistedsupport import gatherResults
-
-
-class KeepAliveSettingSSHServerTransport(SSHServerTransport):
-
-    def connectionMade(self):
-        SSHServerTransport.connectionMade(self)
-        self.transport.setTcpKeepAlive(True)
-
-
-class Factory(SSHFactory):
-    """SSH factory that uses Launchpad's custom authentication.
-
-    This class tells the SSH service to use our custom authentication service
-    and configures the host keys for the SSH server. It also logs connection
-    to and disconnection from the SSH server.
-    """
-
-    protocol = KeepAliveSettingSSHServerTransport
-
-    def __init__(self, portal, private_key, public_key, banner=None):
-        """Construct an SSH factory.
-
-        :param portal: The portal used to turn credentials into users.
-        :param private_key: The private key of the server, must be an RSA
-            key, given as a `twisted.conch.ssh.keys.Key` object.
-        :param public_key: The public key of the server, must be an RSA
-            key, given as a `twisted.conch.ssh.keys.Key` object.
-        :param banner: The text to display when users successfully log in.
-        """
-        # Although 'portal' isn't part of the defined interface for
-        # `SSHFactory`, defining it here is how the `SSHUserAuthServer` gets
-        # at it. (Look for the beautiful line "self.portal =
-        # self.transport.factory.portal").
-        self.portal = portal
-        self.services['ssh-userauth'] = self._makeAuthServer
-        self._private_key = private_key
-        self._public_key = public_key
-        self._banner = banner
-
-    def _makeAuthServer(self, *args, **kwargs):
-        kwargs['banner'] = self._banner
-        return SSHUserAuthServer(*args, **kwargs)
-
-    def buildProtocol(self, address):
-        """Build an SSH protocol instance, logging the event.
-
-        The protocol object we return is slightly modified so that we can hook
-        into the 'connectionLost' event and log the disconnection.
-        """
-        transport = SSHFactory.buildProtocol(self, address)
-        transport._realConnectionLost = transport.connectionLost
-        transport.connectionLost = (
-            lambda reason: self.connectionLost(transport, reason))
-        notify(events.UserConnected(transport, address))
-        return transport
-
-    def connectionLost(self, transport, reason):
-        """Call 'connectionLost' on 'transport', logging the event."""
-        try:
-            return transport._realConnectionLost(reason)
-        finally:
-            # Conch's userauth module sets 'avatar' on the transport if the
-            # authentication succeeded. Thus, if it's not there,
-            # authentication failed. We can't generate this event from the
-            # authentication layer since:
-            #
-            # a) almost every SSH login has at least one failure to
-            # authenticate due to multiple keys on the client-side.
-            #
-            # b) the server doesn't normally generate a "go away" event.
-            # Rather, the client simply stops trying.
-            if getattr(transport, 'avatar', None) is None:
-                notify(events.AuthenticationFailed(transport))
-            notify(events.UserDisconnected(transport))
-
-    def getPublicKeys(self):
-        """Return the server's configured public key.
-
-        See `SSHFactory.getPublicKeys`.
-        """
-        return {'ssh-rsa': self._public_key}
-
-    def getPrivateKeys(self):
-        """Return the server's configured private key.
-
-        See `SSHFactory.getPrivateKeys`.
-        """
-        return {'ssh-rsa': self._private_key}
-
-
-class SSHService(service.Service):
-    """A Twisted service for the SSH server."""
-
-    def __init__(self, portal, private_key_path, public_key_path,
-                 oops_configuration, main_log, access_log,
-                 access_log_path, strport='tcp:22', factory_decorator=None,
-                 banner=None):
-        """Construct an SSH service.
-
-        :param portal: The `twisted.cred.portal.Portal` that turns
-            authentication requests into views on the system.
-        :param private_key_path: The path to the SSH server's private key.
-        :param public_key_path: The path to the SSH server's public key.
-        :param oops_configuration: The section of the configuration file with
-            the OOPS config details for this server.
-        :param main_log: The name of the logger to log most of the server
-            stuff to.
-        :param access_log: The name of the logger object to log the server
-            access details to.
-        :param access_log_path: The path to the access log file.
-        :param strport: The port to run the server on, expressed in Twisted's
-            "strports" mini-language. Defaults to 'tcp:22'.
-        :param factory_decorator: An optional callable that can decorate the
-            server factory (e.g. with a
-            `twisted.protocols.policies.TimeoutFactory`).  It takes one
-            argument, a factory, and must return a factory.
-        :param banner: An announcement printed to users when they connect.
-            By default, announce nothing.
-        """
-        ssh_factory = Factory(
-            portal,
-            private_key=Key.fromFile(private_key_path),
-            public_key=Key.fromFile(public_key_path),
-            banner=banner)
-        if factory_decorator is not None:
-            ssh_factory = factory_decorator(ssh_factory)
-        self.service = strports.service(strport, ssh_factory)
-        self._oops_configuration = oops_configuration
-        self._main_log = main_log
-        self._access_log = access_log
-        self._access_log_path = access_log_path
-
-    def startService(self):
-        """Start the SSH service."""
-        manager = accesslog.LoggingManager(
-            logging.getLogger(self._main_log),
-            logging.getLogger(self._access_log_path),
-            self._access_log_path)
-        manager.setUp()
-        notify(events.ServerStarting())
-        # By default, only the owner of files should be able to write to them.
-        # Perhaps in the future this line will be deleted and the umask
-        # managed by the startup script.
-        os.umask(0022)
-        service.Service.startService(self)
-        self.service.startService()
-
-    def stopService(self):
-        """Stop the SSH service."""
-        deferred = gatherResults([
-            defer.maybeDeferred(service.Service.stopService, self),
-            defer.maybeDeferred(self.service.stopService)])
-
-        def log_stopped(ignored):
-            notify(events.ServerStopped())
-            return ignored
-
-        return deferred.addBoth(log_stopped)

=== removed file 'lib/lp/services/sshserver/session.py'
--- lib/lp/services/sshserver/session.py	2010-08-20 20:31:18 +0000
+++ lib/lp/services/sshserver/session.py	1970-01-01 00:00:00 +0000
@@ -1,124 +0,0 @@
-# Copyright 2010 Canonical Ltd.  This software is licensed under the
-# GNU Affero General Public License version 3 (see the file LICENSE).
-
-"""Patched SSH session for the Launchpad server."""
-
-__metaclass__ = type
-__all__ = [
-    'DoNothingSession',
-    'PatchedSSHSession',
-    ]
-
-from twisted.conch.interfaces import ISession
-from twisted.conch.ssh import (
-    channel,
-    connection,
-    session,
-    )
-from zope.interface import implements
-
-
-class PatchedSSHSession(session.SSHSession, object):
-    """Session adapter that corrects bugs in Conch.
-
-    This object provides no custom logic for Launchpad, it just addresses some
-    simple bugs in the base `session.SSHSession` class that are not yet fixed
-    upstream.
-    """
-
-    def closeReceived(self):
-        # Without this, the client hangs when it's finished transferring.
-        # XXX: JonathanLange 2009-01-05: This does not appear to have a
-        # corresponding bug in Twisted. We should test that the above comment
-        # is indeed correct and then file a bug upstream.
-        self.loseConnection()
-
-    def loseConnection(self):
-        # XXX: JonathanLange 2008-03-31: This deliberately replaces the
-        # implementation of session.SSHSession.loseConnection. The default
-        # implementation will try to call loseConnection on the client
-        # transport even if it's None. I don't know *why* it is None, so this
-        # doesn't necessarily address the root cause.
-        # See http://twistedmatrix.com/trac/ticket/2754.
-        transport = getattr(self.client, 'transport', None)
-        if transport is not None:
-            transport.loseConnection()
-        # This is called by session.SSHSession.loseConnection. SSHChannel is
-        # the base class of SSHSession.
-        channel.SSHChannel.loseConnection(self)
-
-    def stopWriting(self):
-        """See `session.SSHSession.stopWriting`.
-
-        When the client can't keep up with us, we ask the child process to
-        stop giving us data.
-        """
-        # XXX: MichaelHudson 2008-06-27: Being cagey about whether
-        # self.client.transport is entirely paranoia inspired by the comment
-        # in `loseConnection` above. It would be good to know if and why it is
-        # necessary. See http://twistedmatrix.com/trac/ticket/2754.
-        transport = getattr(self.client, 'transport', None)
-        if transport is not None:
-            # For SFTP connections, 'transport' is actually a _DummyTransport
-            # instance. Neither _DummyTransport nor the protocol it wraps
-            # (filetransfer.FileTransferServer) support pausing.
-            pauseProducing = getattr(transport, 'pauseProducing', None)
-            if pauseProducing is not None:
-                pauseProducing()
-
-    def startWriting(self):
-        """See `session.SSHSession.startWriting`.
-
-        The client is ready for data again, so ask the child to start
-        producing data again.
-        """
-        # XXX: MichaelHudson 2008-06-27: Being cagey about whether
-        # self.client.transport is entirely paranoia inspired by the comment
-        # in `loseConnection` above. It would be good to know if and why it is
-        # necessary. See http://twistedmatrix.com/trac/ticket/2754.
-        transport = getattr(self.client, 'transport', None)
-        if transport is not None:
-            # For SFTP connections, 'transport' is actually a _DummyTransport
-            # instance. Neither _DummyTransport nor the protocol it wraps
-            # (filetransfer.FileTransferServer) support pausing.
-            resumeProducing = getattr(transport, 'resumeProducing', None)
-            if resumeProducing is not None:
-                resumeProducing()
-
-
-class DoNothingSession:
-    """A Conch user session that allows nothing."""
-
-    implements(ISession)
-
-    def __init__(self, avatar):
-        self.avatar = avatar
-
-    def closed(self):
-        """See ISession."""
-
-    def eofReceived(self):
-        """See ISession."""
-
-    def errorWithMessage(self, protocol, msg):
-        protocol.session.writeExtended(
-            connection.EXTENDED_DATA_STDERR, msg)
-        protocol.loseConnection()
-
-    def execCommand(self, protocol, command):
-        """See ISession."""
-        self.errorWithMessage(
-            protocol, "Not allowed to execute commands on this server.\r\n")
-
-    def getPty(self, term, windowSize, modes):
-        """See ISession."""
-        # Do nothing, as we don't provide shell access. openShell will get
-        # called and handle this error message and disconnect.
-
-    def openShell(self, protocol):
-        """See ISession."""
-        self.errorWithMessage(protocol, "No shells on this server.\r\n")
-
-    def windowChanged(self, newWindowSize):
-        """See ISession."""
-        raise NotImplementedError(self.windowChanged)

=== removed file 'lib/lp/services/sshserver/sftp.py'
--- lib/lp/services/sshserver/sftp.py	2010-08-20 20:31:18 +0000
+++ lib/lp/services/sshserver/sftp.py	1970-01-01 00:00:00 +0000
@@ -1,45 +0,0 @@
-# Copyright 2010 Canonical Ltd.  This software is licensed under the
-# GNU Affero General Public License version 3 (see the file LICENSE).
-
-"""Generic SFTP server functionality."""
-
-__metaclass__ = type
-__all__ = [
-    'FileIsADirectory',
-    'FileTransferServer',
-    ]
-
-from bzrlib import errors as bzr_errors
-from twisted.conch.ssh import filetransfer
-from zope.event import notify
-
-from lp.services.sshserver import events
-
-
-class FileIsADirectory(bzr_errors.PathError):
-    """Raised when writeChunk is called on a directory.
-
-    This exists mainly to be translated into the appropriate SFTP error.
-    """
-
-    _fmt = 'File is a directory: %(path)r%(extra)s'
-
-
-class FileTransferServer(filetransfer.FileTransferServer):
-    """SFTP protocol implementation that logs key events."""
-
-    def __init__(self, data=None, avatar=None):
-        filetransfer.FileTransferServer.__init__(self, data, avatar)
-        notify(events.SFTPStarted(avatar))
-        self.avatar = avatar
-
-    def connectionLost(self, reason):
-        # This method gets called twice: once from `SSHChannel.closeReceived`
-        # when the client closes the channel and once from `SSHSession.closed`
-        # when the server closes the session. We change the avatar attribute
-        # to avoid logging the `SFTPClosed` event twice.
-        filetransfer.FileTransferServer.connectionLost(self, reason)
-        if self.avatar is not None:
-            avatar = self.avatar
-            self.avatar = None
-            notify(events.SFTPClosed(avatar))

=== removed directory 'lib/lp/services/sshserver/tests'
=== removed file 'lib/lp/services/sshserver/tests/__init__.py'
--- lib/lp/services/sshserver/tests/__init__.py	2010-04-15 14:49:55 +0000
+++ lib/lp/services/sshserver/tests/__init__.py	1970-01-01 00:00:00 +0000
@@ -1,8 +0,0 @@
-# Copyright 2010 Canonical Ltd.  This software is licensed under the
-# GNU Affero General Public License version 3 (see the file LICENSE).
-
-"""Tests for the Launchpad SSH server."""
-
-__metaclass__ = type
-__all__ = []
-

=== removed directory 'lib/lp/services/sshserver/tests/keys'
=== removed file 'lib/lp/services/sshserver/tests/keys/ssh_host_key_rsa'
--- lib/lp/services/sshserver/tests/keys/ssh_host_key_rsa	2010-04-15 15:27:30 +0000
+++ lib/lp/services/sshserver/tests/keys/ssh_host_key_rsa	1970-01-01 00:00:00 +0000
@@ -1,15 +0,0 @@
------BEGIN RSA PRIVATE KEY-----
-MIICXAIBAAKBgQDSSpVRPhCiU9PPuZN7QyJdMOgTVwPyYpZGOHutR/9kxFvOLa39
-nY0Eqo39OTumfZBMEEVqIPadQanO9LcdTnl9/Z4LcBGn09EFQ2y7VUkC6J2dSQtr
-YMY0tV+C5HGZ2oYBWKBl5PZ1RI4+qrJpAMMmINdnF0uEE/x8B1iMWGB3PwIBIwKB
-gQCcN2ebb+8ZgBmwQLazVnFMirsHDXClbc630i/9EOmbUAmvGp6B4sCHH5ytevkc
-l8pHIget7JnxKXbUQMKKzJTCpPwwEyL3ZVDxYXg37WQU74cVf93CjOjChs+hOeS1
-sW5m9JFr9oomL5JWnGXr+TV/kYBCNVW++J1Bckn6kYpH+wJBAPUw0ZunXlBRuugA
-YTSmXUUX+ALu6maDD1t7gAk37waxNQMaH5DMk5R4IQtoxeQgCLqL2yEJUqK1lxOy
-wlp1k8UCQQDbj+Vr4poE9MpYNtPDiDqv2aXe6CJ3p1qQuNE0rSxk+0G9h3ASISRw
-DDLgcapg2xkOvG3pidfAJG9P827XiVszAkEA4CyiYm0jB5suivkIaqa7rOK23hxE
-BfQrTFOoQvFPkRcL5ZQ6Hfzes6EIRPIUA8WEUskC3GBLjXLTRTW5Aj+dCwJBAMi+
-E5XWfjBqx6EcL1OvwKDG/g2hCZH4GEnNjBLneQveZ/29qEsW/L43CfHHAiyrD5hx
-w5OxOklF4h02VrZu9EsCQBEZcTAQOrWnkmp7uBrz1V8nFLAE6zFh/6Wj1Mn8k08j
-pcsLJAhm+qlV7EtV/5rk+v3WcXrKiRIiiC0Ron96Dx0=
------END RSA PRIVATE KEY-----

=== removed file 'lib/lp/services/sshserver/tests/keys/ssh_host_key_rsa.pub'
--- lib/lp/services/sshserver/tests/keys/ssh_host_key_rsa.pub	2010-04-15 15:27:30 +0000
+++ lib/lp/services/sshserver/tests/keys/ssh_host_key_rsa.pub	1970-01-01 00:00:00 +0000
@@ -1,1 +0,0 @@
-ssh-rsa AAAAB3NzaC1yc2EAAAABIwAAAIEA0kqVUT4QolPTz7mTe0MiXTDoE1cD8mKWRjh7rUf/ZMRbzi2t/Z2NBKqN/Tk7pn2QTBBFaiD2nUGpzvS3HU55ff2eC3ARp9PRBUNsu1VJAuidnUkLa2DGNLVfguRxmdqGAVigZeT2dUSOPqqyaQDDJiDXZxdLhBP8fAdYjFhgdz8= andrew@frobozz

=== removed file 'lib/lp/services/sshserver/tests/test_accesslog.py'
--- lib/lp/services/sshserver/tests/test_accesslog.py	2012-03-26 06:08:39 +0000
+++ lib/lp/services/sshserver/tests/test_accesslog.py	1970-01-01 00:00:00 +0000
@@ -1,146 +0,0 @@
-# Copyright 2009 Canonical Ltd.  This software is licensed under the
-# GNU Affero General Public License version 3 (see the file LICENSE).
-
-"""Tests for the logging system of the sshserver."""
-
-__metaclass__ = type
-
-import codecs
-import logging
-from logging.handlers import WatchedFileHandler
-import os
-from StringIO import StringIO
-import sys
-import tempfile
-
-from bzrlib.tests import TestCase as BzrTestCase
-import zope.component.event
-
-from lp.services.sshserver.accesslog import LoggingManager
-from lp.testing import TestCase
-
-
-class LoggingManagerMixin:
-
-    _log_count = 0
-
-    def makeLogger(self, name=None):
-        if name is None:
-            self._log_count += 1
-            name = '%s-%s' % (self.id().split('.')[-1], self._log_count)
-        return logging.getLogger(name)
-
-    def installLoggingManager(self, main_log=None, access_log=None,
-                              access_log_path=None):
-        if main_log is None:
-            main_log = self.makeLogger()
-        if access_log is None:
-            access_log = self.makeLogger()
-        if access_log_path is None:
-            fd, access_log_path = tempfile.mkstemp()
-            os.close(fd)
-            self.addCleanup(os.unlink, access_log_path)
-        manager = LoggingManager(main_log, access_log, access_log_path)
-        manager.setUp()
-        self.addCleanup(manager.tearDown)
-        return manager
-
-
-class TestLoggingBazaarInteraction(BzrTestCase, LoggingManagerMixin):
-
-    def setUp(self):
-        BzrTestCase.setUp(self)
-
-        # Trap stderr.
-        self._real_stderr = sys.stderr
-        sys.stderr = codecs.getwriter('utf8')(StringIO())
-
-    def tearDown(self):
-        sys.stderr = self._real_stderr
-        BzrTestCase.tearDown(self)
-
-    def test_leaves_bzr_handlers_unchanged(self):
-        # Bazaar's log handling is untouched by logging setup.
-        root_handlers = logging.getLogger('').handlers
-        bzr_handlers = logging.getLogger('bzr').handlers
-
-        self.installLoggingManager()
-
-        self.assertEqual(root_handlers, logging.getLogger('').handlers)
-        self.assertEqual(bzr_handlers, logging.getLogger('bzr').handlers)
-
-    def test_log_doesnt_go_to_stderr(self):
-        # Once logging setup is called, any messages logged to the
-        # SSH server logger should *not* be logged to stderr. If they are,
-        # they will appear on the user's terminal.
-        log = self.makeLogger()
-        self.installLoggingManager(log)
-
-        # Make sure that a logged message does not go to stderr.
-        log.info('Hello hello')
-        self.assertEqual(sys.stderr.getvalue(), '')
-
-
-class TestLoggingManager(TestCase, LoggingManagerMixin):
-
-    def test_main_log_handlers(self):
-        # There needs to be at least one handler for the root logger. If there
-        # isn't, we'll get constant errors complaining about the lack of
-        # logging handlers.
-        log = self.makeLogger()
-        self.assertEqual([], log.handlers)
-        self.installLoggingManager(log)
-        self.assertNotEqual([], log.handlers)
-
-    def _get_handlers(self):
-        registrations = (
-            zope.component.getGlobalSiteManager().registeredHandlers())
-        return [
-            registration.factory
-            for registration in registrations]
-
-    def test_set_up_registers_event_handler(self):
-        manager = self.installLoggingManager()
-        self.assertIn(manager._log_event, self._get_handlers())
-
-    def test_teardown_restores_event_handlers(self):
-        handlers = self._get_handlers()
-        manager = self.installLoggingManager()
-        manager.tearDown()
-        self.assertEqual(handlers, self._get_handlers())
-
-    def test_teardown_restores_level(self):
-        log = self.makeLogger()
-        old_level = log.level
-        manager = self.installLoggingManager(log)
-        manager.tearDown()
-        self.assertEqual(old_level, log.level)
-
-    def test_teardown_restores_main_log_handlers(self):
-        # tearDown restores log handlers for the main logger.
-        log = self.makeLogger()
-        handlers = list(log.handlers)
-        manager = self.installLoggingManager(log)
-        manager.tearDown()
-        self.assertEqual(handlers, log.handlers)
-
-    def test_teardown_restores_access_log_handlers(self):
-        # tearDown restores log handlers for the access logger.
-        log = self.makeLogger()
-        handlers = list(log.handlers)
-        manager = self.installLoggingManager(access_log=log)
-        manager.tearDown()
-        self.assertEqual(handlers, log.handlers)
-
-    def test_access_handlers(self):
-        # The logging setup installs a rotatable log handler that logs output
-        # to the SSH server access log.
-        directory = self.makeTemporaryDirectory()
-        access_log = self.makeLogger()
-        access_log_path = os.path.join(directory, 'access.log')
-        self.installLoggingManager(
-            access_log=access_log,
-            access_log_path=access_log_path)
-        [handler] = access_log.handlers
-        self.assertIsInstance(handler, WatchedFileHandler)
-        self.assertEqual(access_log_path, handler.baseFilename)

=== removed file 'lib/lp/services/sshserver/tests/test_auth.py'
--- lib/lp/services/sshserver/tests/test_auth.py	2012-01-01 02:58:52 +0000
+++ lib/lp/services/sshserver/tests/test_auth.py	1970-01-01 00:00:00 +0000
@@ -1,516 +0,0 @@
-# Copyright 2009-2010 Canonical Ltd.  This software is licensed under the
-# GNU Affero General Public License version 3 (see the file LICENSE).
-
-import os
-
-from testtools.deferredruntest import (
-    assert_fails_with,
-    AsynchronousDeferredRunTest,
-    flush_logged_errors,
-    )
-from twisted.conch.checkers import SSHPublicKeyDatabase
-from twisted.conch.error import ConchError
-from twisted.conch.ssh import userauth
-from twisted.conch.ssh.common import (
-    getNS,
-    NS,
-    )
-from twisted.conch.ssh.keys import (
-    BadKeyError,
-    Key,
-    )
-from twisted.conch.ssh.transport import (
-    SSHCiphers,
-    SSHServerTransport,
-    )
-from twisted.cred.error import UnauthorizedLogin
-from twisted.cred.portal import (
-    IRealm,
-    Portal,
-    )
-from twisted.internet import defer
-from twisted.python import failure
-from twisted.python.util import sibpath
-from zope.interface import implements
-
-from lp.services.sshserver import auth
-from lp.services.twistedsupport import suppress_stderr
-from lp.testing import TestCase
-from lp.xmlrpc import faults
-
-
-class MockRealm:
-    """A mock realm for testing userauth.SSHUserAuthServer.
-
-    This realm is not actually used in the course of testing, so calls to
-    requestAvatar will raise an exception.
-    """
-
-    implements(IRealm)
-
-    def requestAvatar(self, avatar_id, mind, *interfaces):
-        user_dict = {
-            'id': avatar_id, 'name': avatar_id, 'teams': [],
-            'initialBranches': []}
-        return (
-            interfaces[0], auth.LaunchpadAvatar(user_dict), lambda: None)
-
-
-class MockSSHTransport(SSHServerTransport):
-    """A mock SSH transport for testing userauth.SSHUserAuthServer.
-
-    SSHUserAuthServer expects an SSH transport which has a factory attribute
-    which in turn has a portal attribute. Because the portal is important for
-    testing authentication, we need to be able to provide an interesting portal
-    object to the SSHUserAuthServer.
-
-    In addition, we want to be able to capture any packets sent over the
-    transport.
-    """
-
-    class Factory:
-        def getService(self, transport, nextService):
-            return lambda: None
-
-    def __init__(self, portal):
-        # In Twisted 8.0.1, Conch's transport starts referring to
-        # currentEncryptions where it didn't before. Provide a dummy value for
-        # it.
-        self.currentEncryptions = SSHCiphers('none', 'none', 'none', 'none')
-        self.packets = []
-        self.factory = self.Factory()
-        self.factory.portal = portal
-
-    def sendPacket(self, messageType, payload):
-        self.packets.append((messageType, payload))
-
-    def setService(self, service):
-        pass
-
-
-class UserAuthServerMixin(object):
-    def setUp(self):
-        self.portal = Portal(MockRealm())
-        self.transport = MockSSHTransport(self.portal)
-        self.user_auth = auth.SSHUserAuthServer(self.transport)
-
-    def _getMessageName(self, message_type):
-        """Get the name of the message for the given message type constant."""
-        return userauth.SSHUserAuthServer.protocolMessages[message_type]
-
-    def assertMessageOrder(self, message_types):
-        """Assert that SSH messages were sent in the given order."""
-        messages = userauth.SSHUserAuthServer.protocolMessages
-        self.assertEqual(
-            [messages[msg_type] for msg_type in message_types],
-            [messages[packet_type]
-             for packet_type, contents in self.transport.packets])
-
-    def assertBannerSent(self, banner_message, expected_language='en'):
-        """Assert that 'banner_message' was sent as an SSH banner."""
-        # Check that we received a BANNER, then a FAILURE.
-        for packet_type, packet_content in self.transport.packets:
-            if packet_type == userauth.MSG_USERAUTH_BANNER:
-                bytes, language, empty = getNS(packet_content, 2)
-                self.assertEqual(banner_message, bytes.decode('UTF8'))
-                self.assertEqual(expected_language, language)
-                self.assertEqual('', empty)
-                break
-        else:
-            self.fail("No banner logged.")
-
-
-class TestUserAuthServer(TestCase, UserAuthServerMixin):
-
-    def setUp(self):
-        TestCase.setUp(self)
-        UserAuthServerMixin.setUp(self)
-
-    def test_sendBanner(self):
-        # sendBanner should send an SSH 'packet' with type MSG_USERAUTH_BANNER
-        # and two fields. The first field is the message itself, and the
-        # second is the language tag.
-        #
-        # sendBanner automatically adds a trailing newline, because openssh
-        # and Twisted don't add one when displaying the banner.
-        #
-        # See RFC 4252, Section 5.4.
-        message = u"test message"
-        self.user_auth.sendBanner(message, language='en-US')
-        self.assertBannerSent(message + '\r\n', 'en-US')
-        self.assertEqual(
-            1, len(self.transport.packets),
-            "More than just banner was sent: %r" % self.transport.packets)
-
-    def test_sendBannerUsesCRLF(self):
-        # sendBanner should make sure that any line breaks in the message are
-        # sent as CR LF pairs.
-        #
-        # See RFC 4252, Section 5.4.
-        self.user_auth.sendBanner(u"test\nmessage")
-        [(messageType, payload)] = self.transport.packets
-        bytes, language, empty = getNS(payload, 2)
-        self.assertEqual(bytes.decode('UTF8'), u"test\r\nmessage\r\n")
-
-    def test_requestRaisesConchError(self):
-        # ssh_USERAUTH_REQUEST should raise a ConchError if tryAuth returns
-        # None. Added to catch a bug noticed by pyflakes.
-        # Whitebox test.
-        def mock_try_auth(kind, user, data):
-            return None
-        def mock_eb_bad_auth(reason):
-            reason.trap(ConchError)
-        tryAuth = self.user_auth.tryAuth
-        self.user_auth.tryAuth = mock_try_auth
-        _ebBadAuth, self.user_auth._ebBadAuth = (self.user_auth._ebBadAuth,
-                                                 mock_eb_bad_auth)
-        self.user_auth.serviceStarted()
-        try:
-            packet = NS('jml') + NS('foo') + NS('public_key') + NS('data')
-            self.user_auth.ssh_USERAUTH_REQUEST(packet)
-        finally:
-            self.user_auth.serviceStopped()
-            self.user_auth.tryAuth = tryAuth
-            self.user_auth._ebBadAuth = _ebBadAuth
-
-
-class MockChecker(SSHPublicKeyDatabase):
-    """A very simple public key checker which rejects all offered credentials.
-
-    Used by TestAuthenticationBannerDisplay to test that errors raised by
-    checkers are sent to SSH clients.
-    """
-
-    error_message = u'error message'
-
-    def requestAvatarId(self, credentials):
-        if credentials.username == 'success':
-            return credentials.username
-        else:
-            return failure.Failure(
-                auth.UserDisplayedUnauthorizedLogin(self.error_message))
-
-
-class TestAuthenticationBannerDisplay(UserAuthServerMixin, TestCase):
-    """Check that auth error information is passed through to the client.
-
-    Normally, SSH servers provide minimal information on failed authentication.
-    With Launchpad, much more user information is public, so it is helpful and
-    not insecure to tell users why they failed to authenticate.
-
-    SSH doesn't provide a standard way of doing this, but the
-    MSG_USERAUTH_BANNER message is allowed and seems appropriate. See RFC 4252,
-    Section 5.4 for more information.
-    """
-
-    run_tests_with = AsynchronousDeferredRunTest
-
-    def setUp(self):
-        UserAuthServerMixin.setUp(self)
-        TestCase.setUp(self)
-        self.portal.registerChecker(MockChecker())
-        self.user_auth.serviceStarted()
-        self.key_data = self._makeKey()
-
-    def tearDown(self):
-        self.user_auth.serviceStopped()
-        TestCase.tearDown(self)
-
-    def _makeKey(self):
-        keydir = sibpath(__file__, 'keys')
-        public_key = Key.fromString(
-            open(os.path.join(keydir, 'ssh_host_key_rsa.pub'), 'rb').read())
-        if isinstance(public_key, str):
-            return chr(0) + NS('rsa') + NS(public_key)
-        else:
-            return chr(0) + NS('rsa') + NS(public_key.blob())
-
-    def requestFailedAuthentication(self):
-        return self.user_auth.ssh_USERAUTH_REQUEST(
-            NS('failure') + NS('') + NS('publickey') + self.key_data)
-
-    def requestSuccessfulAuthentication(self):
-        return self.user_auth.ssh_USERAUTH_REQUEST(
-            NS('success') + NS('') + NS('publickey') + self.key_data)
-
-    def requestUnsupportedAuthentication(self):
-        # Note that it doesn't matter how the checker responds -- the server
-        # doesn't get that far.
-        return self.user_auth.ssh_USERAUTH_REQUEST(
-            NS('success') + NS('') + NS('none') + NS(''))
-
-    def test_bannerNotSentOnSuccess(self):
-        # No banner is printed when the user authenticates successfully.
-        self.user_auth._banner = None
-
-        d = self.requestSuccessfulAuthentication()
-        def check(ignored):
-            # Check that no banner was sent to the user.
-            self.assertMessageOrder([userauth.MSG_USERAUTH_SUCCESS])
-        return d.addCallback(check)
-
-    def test_defaultBannerSentOnSuccess(self):
-        # If a banner was passed to the user auth agent then we send it to the
-        # user when they log in.
-        self.user_auth._banner = "Boogedy boo"
-        d = self.requestSuccessfulAuthentication()
-        def check(ignored):
-            self.assertMessageOrder(
-                [userauth.MSG_USERAUTH_BANNER, userauth.MSG_USERAUTH_SUCCESS])
-            self.assertBannerSent(self.user_auth._banner + '\r\n')
-        return d.addCallback(check)
-
-    def test_defaultBannerSentOnlyOnce(self):
-        # We don't send the banner on each authentication attempt, just on the
-        # first one. It is usual for there to be many authentication attempts
-        # per SSH session.
-        self.user_auth._banner = "Boogedy boo"
-
-        d = self.requestUnsupportedAuthentication()
-        d.addCallback(lambda ignored: self.requestSuccessfulAuthentication())
-
-        def check(ignored):
-            # Check that no banner was sent to the user.
-            self.assertMessageOrder(
-                [userauth.MSG_USERAUTH_FAILURE, userauth.MSG_USERAUTH_BANNER,
-                 userauth.MSG_USERAUTH_SUCCESS])
-            self.assertBannerSent(self.user_auth._banner + '\r\n')
-
-        return d.addCallback(check)
-
-    def test_defaultBannerNotSentOnFailure(self):
-        # Failed authentication attempts do not get the default banner
-        # sent.
-        self.user_auth._banner = "You come away two hundred quid down"
-
-        d = self.requestFailedAuthentication()
-
-        def check(ignored):
-            self.assertMessageOrder(
-                [userauth.MSG_USERAUTH_BANNER, userauth.MSG_USERAUTH_FAILURE])
-            self.assertBannerSent(MockChecker.error_message + '\r\n')
-
-        return d.addCallback(check)
-
-    def test_loggedToBanner(self):
-        # When there's an authentication failure, we display an informative
-        # error message through the SSH authentication protocol 'banner'.
-        d = self.requestFailedAuthentication()
-        def check(ignored):
-            # Check that we received a BANNER, then a FAILURE.
-            self.assertMessageOrder(
-                [userauth.MSG_USERAUTH_BANNER, userauth.MSG_USERAUTH_FAILURE])
-            self.assertBannerSent(MockChecker.error_message + '\r\n')
-        return d.addCallback(check)
-
-    def test_unsupportedAuthMethodNotLogged(self):
-        # Trying various authentication methods is a part of the normal
-        # operation of the SSH authentication protocol. We should not spam the
-        # client with warnings about this, as whenever it becomes a problem,
-        # we can rely on the SSH client itself to report it to the user.
-        d = self.requestUnsupportedAuthentication()
-        def check(ignored):
-            # Check that we received only a FAILRE.
-            self.assertMessageOrder([userauth.MSG_USERAUTH_FAILURE])
-        return d.addCallback(check)
-
-
-class TestPublicKeyFromLaunchpadChecker(TestCase):
-    """Tests for the SSH server authentication mechanism.
-
-    PublicKeyFromLaunchpadChecker accepts the SSH authentication information
-    and contacts the authserver to determine if the given details are valid.
-
-    Any authentication errors are displayed back to the user via an SSH
-    MSG_USERAUTH_BANNER message.
-    """
-
-    run_tests_with = AsynchronousDeferredRunTest
-
-    class FakeAuthenticationEndpoint:
-        """A fake client for enough of `IAuthServer` for this test.
-        """
-
-        valid_user = 'valid_user'
-        no_key_user = 'no_key_user'
-        valid_key = 'valid_key'
-
-        def __init__(self):
-            self.calls = []
-
-        def callRemote(self, function_name, *args, **kwargs):
-            return getattr(
-                self, 'xmlrpc_%s' % function_name)(*args, **kwargs)
-
-        def xmlrpc_getUserAndSSHKeys(self, username):
-            self.calls.append(username)
-            if username == self.valid_user:
-                return defer.succeed({
-                    'name': username,
-                    'keys': [('DSA', self.valid_key.encode('base64'))],
-                    })
-            elif username == self.no_key_user:
-                return defer.succeed({
-                    'name': username,
-                    'keys': [],
-                    })
-            else:
-                try:
-                    raise faults.NoSuchPersonWithName(username)
-                except faults.NoSuchPersonWithName:
-                    return defer.fail()
-
-    def makeCredentials(self, username, public_key, mind=None):
-        if mind is None:
-            mind = auth.UserDetailsMind()
-        return auth.SSHPrivateKeyWithMind(
-            username, 'ssh-dss', public_key, '', None, mind)
-
-    def makeChecker(self, do_signature_checking=False):
-        """Construct a PublicKeyFromLaunchpadChecker.
-
-        :param do_signature_checking: if False, as is the default, monkeypatch
-            the returned instance to not verify the signatures of the keys.
-        """
-        checker = auth.PublicKeyFromLaunchpadChecker(self.authserver)
-        if not do_signature_checking:
-            checker._cbRequestAvatarId = self._cbRequestAvatarId
-        return checker
-
-    def _cbRequestAvatarId(self, is_key_valid, credentials):
-        if is_key_valid:
-            return credentials.username
-        return failure.Failure(UnauthorizedLogin())
-
-    def setUp(self):
-        TestCase.setUp(self)
-        self.authserver = self.FakeAuthenticationEndpoint()
-
-    def test_successful(self):
-        # Attempting to log in with a username and key known to the
-        # authentication end-point succeeds.
-        creds = self.makeCredentials(
-            self.authserver.valid_user, self.authserver.valid_key)
-        checker = self.makeChecker()
-        d = checker.requestAvatarId(creds)
-        return d.addCallback(self.assertEqual, self.authserver.valid_user)
-
-    @suppress_stderr
-    def test_invalid_signature(self):
-        # The checker requests attempts to authenticate if the requests have
-        # an invalid signature.
-        creds = self.makeCredentials(
-            self.authserver.valid_user, self.authserver.valid_key)
-        creds.signature = 'a'
-        checker = self.makeChecker(True)
-        d = checker.requestAvatarId(creds)
-        def flush_errback(f):
-            flush_logged_errors(BadKeyError)
-            return f
-        d.addErrback(flush_errback)
-        return assert_fails_with(d, UnauthorizedLogin)
-
-    def assertLoginError(self, checker, creds, error_message):
-        """Logging in with 'creds' against 'checker' fails with 'message'.
-
-        In particular, this tests that the login attempt fails in a way that
-        is sent to the client.
-
-        :param checker: The `ICredentialsChecker` used.
-        :param creds: SSHPrivateKey credentials.
-        :param error_message: String excepted to match the exception's message.
-        :return: Deferred. You must return this from your test.
-        """
-        d = assert_fails_with(
-            checker.requestAvatarId(creds),
-            auth.UserDisplayedUnauthorizedLogin)
-        d.addCallback(
-            lambda exception: self.assertEqual(str(exception), error_message))
-        return d
-
-    def test_noSuchUser(self):
-        # When someone signs in with a non-existent user, they should be told
-        # that. The usual security issues don't apply here because the list of
-        # Launchpad user names is public.
-        checker = self.makeChecker()
-        creds = self.makeCredentials(
-            'no-such-user', self.authserver.valid_key)
-        return self.assertLoginError(
-            checker, creds, 'No such Launchpad account: no-such-user')
-
-    def test_noKeys(self):
-        # When you sign into an existing account with no SSH keys, the SSH
-        # server informs you that the account has no keys.
-        checker = self.makeChecker()
-        creds = self.makeCredentials(
-            self.authserver.no_key_user, self.authserver.valid_key)
-        return self.assertLoginError(
-            checker, creds,
-            "Launchpad user %r doesn't have a registered SSH key"
-            % self.authserver.no_key_user)
-
-    def test_wrongKey(self):
-        # When you sign into an existing account using the wrong key, you
-        # are *not* informed of the wrong key. This is because SSH often
-        # tries several keys as part of normal operation.
-        checker = self.makeChecker()
-        creds = self.makeCredentials(
-            self.authserver.valid_user, 'invalid key')
-        # We cannot use assertLoginError because we are checking that we fail
-        # with UnauthorizedLogin and not its subclass
-        # UserDisplayedUnauthorizedLogin.
-        d = assert_fails_with(
-            checker.requestAvatarId(creds),
-            UnauthorizedLogin)
-        d.addCallback(
-            lambda exception:
-            self.failIf(isinstance(exception,
-                                   auth.UserDisplayedUnauthorizedLogin),
-                        "Should not be a UserDisplayedUnauthorizedLogin"))
-        return d
-
-    def test_successful_with_second_key_calls_authserver_once(self):
-        # It is normal in SSH authentication to be presented with a number of
-        # keys.  When the valid key is presented after some invalid ones (a)
-        # the login succeeds and (b) only one call is made to the authserver
-        # to retrieve the user's details.
-        checker = self.makeChecker()
-        mind = auth.UserDetailsMind()
-        wrong_key_creds = self.makeCredentials(
-            self.authserver.valid_user, 'invalid key', mind)
-        right_key_creds = self.makeCredentials(
-            self.authserver.valid_user, self.authserver.valid_key, mind)
-        d = checker.requestAvatarId(wrong_key_creds)
-        def try_second_key(failure):
-            failure.trap(UnauthorizedLogin)
-            return checker.requestAvatarId(right_key_creds)
-        d.addErrback(try_second_key)
-        d.addCallback(self.assertEqual, self.authserver.valid_user)
-        def check_one_call(r):
-            self.assertEqual(
-                [self.authserver.valid_user], self.authserver.calls)
-            return r
-        d.addCallback(check_one_call)
-        return d
-
-    def test_noSuchUser_with_two_keys_calls_authserver_once(self):
-        # When more than one key is presented for a username that does not
-        # exist, only one call is made to the authserver.
-        checker = self.makeChecker()
-        mind = auth.UserDetailsMind()
-        creds_1 = self.makeCredentials(
-            'invalid-user', 'invalid key 1', mind)
-        creds_2 = self.makeCredentials(
-            'invalid-user', 'invalid key 2', mind)
-        d = checker.requestAvatarId(creds_1)
-        def try_second_key(failure):
-            return assert_fails_with(
-                checker.requestAvatarId(creds_2),
-                UnauthorizedLogin)
-        d.addErrback(try_second_key)
-        def check_one_call(r):
-            self.assertEqual(
-                ['invalid-user'], self.authserver.calls)
-            return r
-        d.addCallback(check_one_call)
-        return d

=== removed file 'lib/lp/services/sshserver/tests/test_events.py'
--- lib/lp/services/sshserver/tests/test_events.py	2011-08-12 11:37:08 +0000
+++ lib/lp/services/sshserver/tests/test_events.py	1970-01-01 00:00:00 +0000
@@ -1,91 +0,0 @@
-# Copyright 2010 Canonical Ltd.  This software is licensed under the
-# GNU Affero General Public License version 3 (see the file LICENSE).
-
-"""Tests for the logging events."""
-
-__metaclass__ = type
-
-import logging
-
-from zope.component import (
-    adapter,
-    getGlobalSiteManager,
-    provideHandler,
-    )
-# This non-standard import is necessary to hook up the event system.
-import zope.component.event
-from zope.event import notify
-
-from lp.services.sshserver.events import (
-    ILoggingEvent,
-    LoggingEvent,
-    )
-from lp.testing import TestCase
-
-
-class ListHandler(logging.Handler):
-    """Logging handler that just appends records to a list.
-
-    This handler isn't intended to be used by production code -- memory leak
-    city! -- instead it's useful for unit tests that want to make sure the
-    right events are being logged.
-    """
-
-    def __init__(self, logging_list):
-        """Construct a `ListHandler`.
-
-        :param logging_list: A list that will be appended to. The handler
-             mutates this list.
-        """
-        logging.Handler.__init__(self)
-        self._list = logging_list
-
-    def emit(self, record):
-        """Append 'record' to the list."""
-        self._list.append(record)
-
-
-class TestLoggingEvent(TestCase):
-
-    def assertLogs(self, records, function, *args, **kwargs):
-        """Assert 'function' logs 'records' when run with the given args."""
-        logged_events = []
-        handler = ListHandler(logged_events)
-        self.logger.addHandler(handler)
-        result = function(*args, **kwargs)
-        self.logger.removeHandler(handler)
-        self.assertEqual(
-            [(record.levelno, record.getMessage())
-             for record in logged_events], records)
-        return result
-
-    def assertEventLogs(self, record, logging_event):
-        self.assertLogs([record], notify, logging_event)
-
-    def setUp(self):
-        TestCase.setUp(self)
-        logger = logging.getLogger(self.factory.getUniqueString())
-        logger.setLevel(logging.DEBUG)
-        self.logger = logger
-
-        @adapter(ILoggingEvent)
-        def _log_event(event):
-            logger.log(event.level, event.message)
-
-        provideHandler(_log_event)
-        self.addCleanup(getGlobalSiteManager().unregisterHandler, _log_event)
-
-    def test_level(self):
-        event = LoggingEvent(logging.CRITICAL, "foo")
-        self.assertEventLogs((logging.CRITICAL, 'foo'), event)
-
-    def test_formatting(self):
-        event = LoggingEvent(logging.DEBUG, "foo: %(name)s", name="bar")
-        self.assertEventLogs((logging.DEBUG, 'foo: bar'), event)
-
-    def test_subclass(self):
-        class SomeEvent(LoggingEvent):
-            template = "%(something)s happened."
-            level = logging.INFO
-        self.assertEventLogs(
-            (logging.INFO, 'foo happened.'), SomeEvent(something='foo'))

=== removed file 'lib/lp/services/sshserver/tests/test_session.py'
--- lib/lp/services/sshserver/tests/test_session.py	2011-08-12 11:37:08 +0000
+++ lib/lp/services/sshserver/tests/test_session.py	1970-01-01 00:00:00 +0000
@@ -1,99 +0,0 @@
-# Copyright 2010 Canonical Ltd.  This software is licensed under the
-# GNU Affero General Public License version 3 (see the file LICENSE).
-
-"""Tests for generic SSH session support."""
-
-__metaclass__ = type
-
-from twisted.conch.interfaces import ISession
-from twisted.conch.ssh import connection
-
-from lp.services.sshserver.session import DoNothingSession
-from lp.testing import TestCase
-
-
-class MockSSHSession:
-    """Just enough of SSHSession to allow checking of reporting to stderr."""
-
-    def __init__(self, log):
-        self.log = log
-
-    def writeExtended(self, channel, data):
-        self.log.append(('writeExtended', channel, data))
-
-
-class MockProcessTransport:
-    """Mock transport used to fake speaking with child processes that are
-    mocked out in tests.
-    """
-
-    def __init__(self, executable):
-        self._executable = executable
-        self.log = []
-        self.session = MockSSHSession(self.log)
-
-    def closeStdin(self):
-        self.log.append(('closeStdin',))
-
-    def loseConnection(self):
-        self.log.append(('loseConnection',))
-
-    def signalProcess(self, signal):
-        self.log.append(('signalProcess', signal))
-
-    def write(self, data):
-        self.log.append(('write', data))
-
-
-class TestDoNothing(TestCase):
-    """Tests for DoNothingSession."""
-
-    def setUp(self):
-        super(TestDoNothing, self).setUp()
-        self.session = DoNothingSession(None)
-
-    def test_getPtyIsANoOp(self):
-        # getPty is called on the way to establishing a shell. Since we don't
-        # give out shells, it should be a no-op. Raising an exception would
-        # log an OOPS, so we won't do that.
-        self.assertEqual(None, self.session.getPty(None, None, None))
-
-    def test_openShellNotImplemented(self):
-        # openShell closes the connection.
-        protocol = MockProcessTransport('bash')
-        self.session.openShell(protocol)
-        self.assertEqual(
-            [('writeExtended', connection.EXTENDED_DATA_STDERR,
-              'No shells on this server.\r\n'),
-             ('loseConnection',)],
-            protocol.log)
-
-    def test_windowChangedNotImplemented(self):
-        # windowChanged raises a NotImplementedError. It doesn't matter what
-        # we pass it.
-        self.assertRaises(NotImplementedError,
-                          self.session.windowChanged, None)
-
-    def test_providesISession(self):
-        # DoNothingSession must provide ISession.
-        self.failUnless(ISession.providedBy(self.session),
-                        "DoNothingSession doesn't implement ISession")
-
-    def test_closedDoesNothing(self):
-        # closed is a no-op.
-        self.assertEqual(None, self.session.closed())
-
-    def test_execCommandNotImplemented(self):
-        # DoNothingSession.execCommand spawns the appropriate process.
-        protocol = MockProcessTransport('bash')
-        command = 'cat /etc/hostname'
-        self.session.execCommand(protocol, command)
-        self.assertEqual(
-            [('writeExtended', connection.EXTENDED_DATA_STDERR,
-              'Not allowed to execute commands on this server.\r\n'),
-             ('loseConnection',)],
-            protocol.log)
-
-    def test_eofReceivedDoesNothingWhenNoCommand(self):
-        # When no process has been created, 'eofReceived' is a no-op.
-        self.assertEqual(None, self.session.eofReceived())

=== modified file 'setup.py'
--- setup.py	2014-01-17 02:01:54 +0000
+++ setup.py	2015-01-06 12:50:32 +0000
@@ -55,6 +55,7 @@
         'lazr.restful',
         'lazr.jobrunner',
         'lazr.smtptest',
+        'lazr.sshserver',
         'lazr.testing',
         'lazr.uri',
         'lpjsmin',

=== modified file 'versions.cfg'
--- versions.cfg	2014-12-08 02:23:41 +0000
+++ versions.cfg	2015-01-06 12:50:32 +0000
@@ -53,6 +53,7 @@
 lazr.restful = 0.19.10
 lazr.restfulclient = 0.13.2
 lazr.smtptest = 1.3
+lazr.sshserver = 0.1
 lazr.testing = 0.1.1
 lazr.uri = 1.0.2
 lpjsmin = 0.5


Follow ups