Merge lp:~jml/launchpad/codehosting-to-services into lp:launchpad

Proposed by Jonathan Lange
Status: Merged
Merged at revision: not available
Proposed branch: lp:~jml/launchpad/codehosting-to-services
Merge into: lp:launchpad
Prerequisite: lp:~jml/launchpad/extract-ssh-server-auth
Diff against target: 594 lines (+191/-134) (has conflicts)
18 files modified
daemons/sftp.tac (+1/-1)
lib/lp/codehosting/sftp.py (+1/-23)
lib/lp/codehosting/sshserver/daemon.py (+3/-3)
lib/lp/codehosting/sshserver/session.py (+19/-75)
lib/lp/codehosting/sshserver/tests/test_daemon.py (+2/-2)
lib/lp/services/sshserver/__init__.py (+8/-0)
lib/lp/services/sshserver/accesslog.py (+1/-1)
lib/lp/services/sshserver/auth.py (+6/-5)
lib/lp/services/sshserver/events.py (+2/-13)
lib/lp/services/sshserver/service.py (+3/-3)
lib/lp/services/sshserver/session.py (+79/-0)
lib/lp/services/sshserver/sftp.py (+35/-0)
lib/lp/services/sshserver/tests/__init__.py (+8/-0)
lib/lp/services/sshserver/tests/keys/ssh_host_key_rsa (+15/-0)
lib/lp/services/sshserver/tests/keys/ssh_host_key_rsa.pub (+1/-0)
lib/lp/services/sshserver/tests/test_accesslog.py (+4/-4)
lib/lp/services/sshserver/tests/test_auth.py (+2/-3)
lib/lp/services/sshserver/tests/test_events.py (+1/-1)
Text conflict in daemons/sftp.tac
Text conflict in lib/lp/services/sshserver/accesslog.py
Text conflict in lib/lp/services/sshserver/service.py
Text conflict in lib/lp/services/sshserver/tests/test_auth.py
To merge this branch: bzr merge lp:~jml/launchpad/codehosting-to-services
Reviewer Review Type Date Requested Status
Paul Hummer (community) code Approve
Review via email: mp+23491@code.launchpad.net

Commit message

Create a new generic Launchpad SSH server in lp.services.

Description of the change

As best as I can tell, this branch completes the work done in the mega lp:~jml/launchpad/ssh-key-auth branch. It moves all of the generic code out of lp.codehosting.sshserver and into the new lp.services.sshserver.

Most of the changes are simple module moves. There are some exceptions though.

 * PatchedSSHSession is extracted from lp.codehosting.sshserver.session and put into lp.services. It's the only generic chunk from the original module. It's also pretty much necessary for running a production SSH service.

 * Likewise, FileTransferServer is moved out of lp.codehosting.sftp and into lp.services. It is modified slightly so that the SFTPStarted event is generated from it. Behaviour should be equivalent to current behaviour.

 * BazaarSSHStarted and BazaarSSHClosed events are moved to the lp.codehosting session module. Relevant docstrings are updated.

 * I had to copy some keys over for testing.

 * Some tests and docstrings were tweaked to refer to Launchpad SSH server, rather than codehosting.

I look forward to your review.

jml

To post a comment you must log in.
Revision history for this message
Paul Hummer (rockstar) wrote :

<rockstar> jml, all of the codehosting-to-services branch is moves right? No real changes?
<jml> rockstar, pretty much. except for what I mention in my comment.
<rockstar> jml, okay, so basically just docstrings and test comments got changed (outside of imports)
<jml> rockstar, that sounds right.
<rockstar> jml, okay. Looking now.
<rockstar> jml, your comment says you had to copy keys over. Why couldn't you move them?
<jml> rockstar, they are also used by the codehosting acceptance tests.
<rockstar> jml, hm. Do you really think it's wise to duplicate those keys?
<jml> rockstar, why would it be unwise?
<rockstar> jml, duplication. It's not code, this I know. But duplication in general can become unfun.
<jml> rockstar, you want me to generate a new RSA SSH key pair?
<jml> rockstar, it's pretty easy. there's even a cool website that'll do it for you!
<rockstar> jml, no, we can use the same one, but I'm wondering if we could symlink or something.
<rockstar> jml, that sounds like a very silly website. :)
<jml> rockstar, it is – http://sshkeygen.com/
* rockstar groans
<jml> rockstar, anyway, I can make them symlinks if you want. it doesn't matter much either way to me
<rockstar> jml, actually, it doesn't matter much to me either. The question was more or less one of Kiko's "exploratory" questions. If you're okay with it, I am.
<jml> rockstar, cool.

review: Approve (code)

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'daemons/sftp.tac'
2--- daemons/sftp.tac 2010-04-16 19:01:12 +0000
3+++ daemons/sftp.tac 2010-04-16 19:01:18 +0000
4@@ -21,7 +21,7 @@
5 from lp.codehosting.sshserver.daemon import (
6 ACCESS_LOG_NAME, get_key_path, LOG_NAME, make_portal, OOPS_CONFIG_SECTION,
7 PRIVATE_KEY_FILE, PUBLIC_KEY_FILE)
8-from lp.codehosting.sshserver.service import SSHService
9+from lp.services.sshserver.service import SSHService
10
11
12 # Construct an Application that has the codehosting SSH server.
13
14=== modified file 'lib/lp/codehosting/sftp.py'
15--- lib/lp/codehosting/sftp.py 2010-04-16 19:01:12 +0000
16+++ lib/lp/codehosting/sftp.py 2010-04-16 19:01:18 +0000
17@@ -15,7 +15,6 @@
18 __metaclass__ = type
19 __all__ = [
20 'avatar_to_sftp_server',
21- 'FileTransferServer',
22 'TransportSFTPServer',
23 ]
24
25@@ -34,12 +33,10 @@
26 from twisted.internet import defer
27 from twisted.python import util
28
29-from zope.event import notify
30 from zope.interface import implements
31
32+from canonical.config import config
33 from lp.codehosting.vfs import AsyncLaunchpadTransport, LaunchpadServer
34-from lp.codehosting.sshserver import events
35-from canonical.config import config
36 from lp.services.twistedsupport import gatherResults
37
38
39@@ -253,28 +250,9 @@
40 avatar.branchfs_proxy, user_id, hosted_transport, mirror_transport)
41 server.start_server()
42 transport = AsyncLaunchpadTransport(server, server.get_url())
43- notify(events.SFTPStarted(avatar))
44 return TransportSFTPServer(transport)
45
46
47-class FileTransferServer(filetransfer.FileTransferServer):
48-
49- def __init__(self, data=None, avatar=None):
50- filetransfer.FileTransferServer.__init__(self, data, avatar)
51- self.avatar = avatar
52-
53- def connectionLost(self, reason):
54- # This method gets called twice: once from `SSHChannel.closeReceived`
55- # when the client closes the channel and once from `SSHSession.closed`
56- # when the server closes the session. We change the avatar attribute
57- # to avoid logging the `SFTPClosed` event twice.
58- filetransfer.FileTransferServer.connectionLost(self, reason)
59- if self.avatar is not None:
60- avatar = self.avatar
61- self.avatar = None
62- notify(events.SFTPClosed(avatar))
63-
64-
65 class TransportSFTPServer:
66 """An implementation of `ISFTPServer` that backs onto a Bazaar transport.
67
68
69=== modified file 'lib/lp/codehosting/sshserver/daemon.py'
70--- lib/lp/codehosting/sshserver/daemon.py 2010-04-16 19:01:12 +0000
71+++ lib/lp/codehosting/sshserver/daemon.py 2010-04-16 19:01:18 +0000
72@@ -27,9 +27,9 @@
73
74 from canonical.config import config
75 from lp.codehosting import sftp
76-from lp.codehosting.sshserver.auth import (
77+from lp.codehosting.sshserver.session import launch_smart_server
78+from lp.services.sshserver.auth import (
79 LaunchpadAvatar, PublicKeyFromLaunchpadChecker)
80-from lp.codehosting.sshserver.session import launch_smart_server
81
82
83 # The names of the key files of the server itself. The directory itself is
84@@ -97,7 +97,7 @@
85 """Create and return a `Portal` for the SSH service.
86
87 This portal accepts SSH credentials and returns our customized SSH
88- avatars (see `lp.codehosting.sshserver.auth.CodehostingAvatar`).
89+ avatars (see `CodehostingAvatar`).
90 """
91 authentication_proxy = Proxy(
92 config.codehosting.authentication_endpoint)
93
94=== modified file 'lib/lp/codehosting/sshserver/session.py'
95--- lib/lp/codehosting/sshserver/session.py 2010-04-16 19:01:12 +0000
96+++ lib/lp/codehosting/sshserver/session.py 2010-04-16 19:01:18 +0000
97@@ -6,7 +6,6 @@
98 __metaclass__ = type
99 __all__ = [
100 'launch_smart_server',
101- 'PatchedSSHSession',
102 ]
103
104 import os
105@@ -16,81 +15,23 @@
106 from zope.interface import implements
107
108 from twisted.conch.interfaces import ISession
109-from twisted.conch.ssh import channel, connection, session
110+from twisted.conch.ssh import connection
111 from twisted.internet.process import ProcessExitedAlready
112 from twisted.python import log
113
114 from canonical.config import config
115 from lp.codehosting import get_bzr_path
116-from lp.codehosting.sshserver import events
117-
118-
119-class PatchedSSHSession(session.SSHSession, object):
120- """Session adapter that corrects bugs in Conch.
121-
122- This object provides no custom logic for Launchpad, it just addresses some
123- simple bugs in the base `session.SSHSession` class that are not yet fixed
124- upstream.
125- """
126-
127- def closeReceived(self):
128- # Without this, the client hangs when it's finished transferring.
129- # XXX: JonathanLange 2009-01-05: This does not appear to have a
130- # corresponding bug in Twisted. We should test that the above comment
131- # is indeed correct and then file a bug upstream.
132- self.loseConnection()
133-
134- def loseConnection(self):
135- # XXX: JonathanLange 2008-03-31: This deliberately replaces the
136- # implementation of session.SSHSession.loseConnection. The default
137- # implementation will try to call loseConnection on the client
138- # transport even if it's None. I don't know *why* it is None, so this
139- # doesn't necessarily address the root cause.
140- # See http://twistedmatrix.com/trac/ticket/2754.
141- transport = getattr(self.client, 'transport', None)
142- if transport is not None:
143- transport.loseConnection()
144- # This is called by session.SSHSession.loseConnection. SSHChannel is
145- # the base class of SSHSession.
146- channel.SSHChannel.loseConnection(self)
147-
148- def stopWriting(self):
149- """See `session.SSHSession.stopWriting`.
150-
151- When the client can't keep up with us, we ask the child process to
152- stop giving us data.
153- """
154- # XXX: MichaelHudson 2008-06-27: Being cagey about whether
155- # self.client.transport is entirely paranoia inspired by the comment
156- # in `loseConnection` above. It would be good to know if and why it is
157- # necessary. See http://twistedmatrix.com/trac/ticket/2754.
158- transport = getattr(self.client, 'transport', None)
159- if transport is not None:
160- # For SFTP connections, 'transport' is actually a _DummyTransport
161- # instance. Neither _DummyTransport nor the protocol it wraps
162- # (filetransfer.FileTransferServer) support pausing.
163- pauseProducing = getattr(transport, 'pauseProducing', None)
164- if pauseProducing is not None:
165- pauseProducing()
166-
167- def startWriting(self):
168- """See `session.SSHSession.startWriting`.
169-
170- The client is ready for data again, so ask the child to start
171- producing data again.
172- """
173- # XXX: MichaelHudson 2008-06-27: Being cagey about whether
174- # self.client.transport is entirely paranoia inspired by the comment
175- # in `loseConnection` above. It would be good to know if and why it is
176- # necessary. See http://twistedmatrix.com/trac/ticket/2754.
177- transport = getattr(self.client, 'transport', None)
178- if transport is not None:
179- # For SFTP connections, 'transport' is actually a _DummyTransport
180- # instance. Neither _DummyTransport nor the protocol it wraps
181- # (filetransfer.FileTransferServer) support pausing.
182- resumeProducing = getattr(transport, 'resumeProducing', None)
183- if resumeProducing is not None:
184- resumeProducing()
185+from lp.services.sshserver.events import AvatarEvent
186+
187+
188+class BazaarSSHStarted(AvatarEvent):
189+
190+ template = '[%(session_id)s] %(username)s started bzr+ssh session.'
191+
192+
193+class BazaarSSHClosed(AvatarEvent):
194+
195+ template = '[%(session_id)s] %(username)s closed bzr+ssh session.'
196
197
198 class ForbiddenCommand(Exception):
199@@ -116,7 +57,10 @@
200 def closed(self):
201 """See ISession."""
202 if self._transport is not None:
203- notify(events.BazaarSSHClosed(self.avatar))
204+ # XXX: JonathanLange 2010-04-15: This is something of an
205+ # abstraction violation. Apart from this line and its twin, this
206+ # class knows nothing about Bazaar.
207+ notify(BazaarSSHClosed(self.avatar))
208 try:
209 self._transport.signalProcess('HUP')
210 except (OSError, ProcessExitedAlready):
211@@ -154,9 +98,9 @@
212 "ERROR: %r already running a command on transport %r"
213 % (self, self._transport))
214 # XXX: JonathanLange 2008-12-23: This is something of an abstraction
215- # violation. Apart from this line, this class knows nothing about
216- # Bazaar.
217- notify(events.BazaarSSHStarted(self.avatar))
218+ # violation. Apart from this line and its twin, this class knows
219+ # nothing about Bazaar.
220+ notify(BazaarSSHStarted(self.avatar))
221 self._transport = self.reactor.spawnProcess(
222 protocol, executable, arguments, env=self.environment)
223
224
225=== modified file 'lib/lp/codehosting/sshserver/tests/test_daemon.py'
226--- lib/lp/codehosting/sshserver/tests/test_daemon.py 2010-04-16 19:01:12 +0000
227+++ lib/lp/codehosting/sshserver/tests/test_daemon.py 2010-04-16 19:01:18 +0000
228@@ -14,10 +14,10 @@
229
230 from canonical.testing.layers import TwistedLayer
231
232-from lp.codehosting.sshserver.auth import SSHUserAuthServer
233 from lp.codehosting.sshserver.daemon import (
234 get_key_path, get_portal, PRIVATE_KEY_FILE, PUBLIC_KEY_FILE)
235-from lp.codehosting.sshserver.service import Factory
236+from lp.services.sshserver.auth import SSHUserAuthServer
237+from lp.services.sshserver.service import Factory
238
239
240 class StringTransportWith_setTcpKeepAlive(StringTransport):
241
242=== added directory 'lib/lp/services/sshserver'
243=== added file 'lib/lp/services/sshserver/__init__.py'
244--- lib/lp/services/sshserver/__init__.py 1970-01-01 00:00:00 +0000
245+++ lib/lp/services/sshserver/__init__.py 2010-04-16 19:01:18 +0000
246@@ -0,0 +1,8 @@
247+# Copyright 2010 Canonical Ltd. This software is licensed under the
248+# GNU Affero General Public License version 3 (see the file LICENSE).
249+
250+"""The Launchpad SSH server."""
251+
252+__metaclass__ = type
253+__all__ = []
254+
255
256=== renamed file 'lib/lp/codehosting/sshserver/accesslog.py' => 'lib/lp/services/sshserver/accesslog.py'
257--- lib/lp/codehosting/sshserver/accesslog.py 2010-04-16 19:01:12 +0000
258+++ lib/lp/services/sshserver/accesslog.py 2010-04-16 19:01:18 +0000
259@@ -17,7 +17,7 @@
260 import zope.component.event
261
262 from canonical.launchpad.scripts import WatchedFileHandler
263-from lp.codehosting.sshserver.events import ILoggingEvent
264+from lp.services.sshserver.events import ILoggingEvent
265 from lp.services.utils import synchronize
266
267
268
269=== renamed file 'lib/lp/codehosting/sshserver/auth.py' => 'lib/lp/services/sshserver/auth.py'
270--- lib/lp/codehosting/sshserver/auth.py 2010-04-16 19:01:12 +0000
271+++ lib/lp/services/sshserver/auth.py 2010-04-16 19:01:18 +0000
272@@ -38,11 +38,12 @@
273 from zope.event import notify
274 from zope.interface import implements
275
276-from lp.codehosting import sftp
277-from lp.codehosting.sshserver import events
278-from lp.codehosting.sshserver.session import PatchedSSHSession
279+from canonical.launchpad.xmlrpc import faults
280+
281+from lp.services.sshserver import events
282+from lp.services.sshserver.sftp import FileTransferServer
283+from lp.services.sshserver.session import PatchedSSHSession
284 from lp.services.twistedsupport.xmlrpc import trap_fault
285-from canonical.launchpad.xmlrpc import faults
286
287
288 class LaunchpadAvatar(avatar.ConchUser):
289@@ -68,7 +69,7 @@
290 # fixes).
291 self.channelLookup = {'session': PatchedSSHSession}
292 # ...and set the only subsystem to be SFTP.
293- self.subsystemLookup = {'sftp': sftp.FileTransferServer}
294+ self.subsystemLookup = {'sftp': FileTransferServer}
295
296 def logout(self):
297 notify(events.UserLoggedOut(self))
298
299=== renamed file 'lib/lp/codehosting/sshserver/events.py' => 'lib/lp/services/sshserver/events.py'
300--- lib/lp/codehosting/sshserver/events.py 2010-04-16 19:01:12 +0000
301+++ lib/lp/services/sshserver/events.py 2010-04-16 19:01:18 +0000
302@@ -6,8 +6,7 @@
303 __metaclass__ = type
304 __all__ = [
305 'AuthenticationFailed',
306- 'BazaarSSHClosed',
307- 'BazaarSSHStarted',
308+ 'AvatarEvent',
309 'ILoggingEvent',
310 'LoggingEvent',
311 'ServerStarting',
312@@ -28,7 +27,7 @@
313 class ILoggingEvent(Interface):
314 """An event is a logging event if it has a message and a severity level.
315
316- Events that provide this interface will be logged in codehosting access
317+ Events that provide this interface will be logged in the SSH server access
318 log.
319 """
320
321@@ -143,13 +142,3 @@
322 class SFTPClosed(AvatarEvent):
323
324 template = '[%(session_id)s] %(username)s closed SFTP session.'
325-
326-
327-class BazaarSSHStarted(AvatarEvent):
328-
329- template = '[%(session_id)s] %(username)s started bzr+ssh session.'
330-
331-
332-class BazaarSSHClosed(AvatarEvent):
333-
334- template = '[%(session_id)s] %(username)s closed bzr+ssh session.'
335
336=== renamed file 'lib/lp/codehosting/sshserver/service.py' => 'lib/lp/services/sshserver/service.py'
337--- lib/lp/codehosting/sshserver/service.py 2010-04-16 19:01:12 +0000
338+++ lib/lp/services/sshserver/service.py 2010-04-16 19:01:18 +0000
339@@ -1,7 +1,7 @@
340 # Copyright 2009 Canonical Ltd. This software is licensed under the
341 # GNU Affero General Public License version 3 (see the file LICENSE).
342
343-"""Twisted `service.Service` class for the codehosting SSH server.
344+"""Twisted `service.Service` class for the Launchpad SSH server.
345
346 An `SSHService` object can be used to launch the SSH server.
347 """
348@@ -24,8 +24,8 @@
349
350 from zope.event import notify
351
352-from lp.codehosting.sshserver import accesslog, events
353-from lp.codehosting.sshserver.auth import SSHUserAuthServer
354+from lp.services.sshserver import accesslog, events
355+from lp.services.sshserver.auth import SSHUserAuthServer
356 from lp.services.twistedsupport import gatherResults
357 from lp.services.twistedsupport.loggingsupport import set_up_oops_reporting
358
359
360=== added file 'lib/lp/services/sshserver/session.py'
361--- lib/lp/services/sshserver/session.py 1970-01-01 00:00:00 +0000
362+++ lib/lp/services/sshserver/session.py 2010-04-16 19:01:18 +0000
363@@ -0,0 +1,79 @@
364+# Copyright 2010 Canonical Ltd. This software is licensed under the
365+# GNU Affero General Public License version 3 (see the file LICENSE).
366+
367+"""Patched SSH session for the Launchpad server."""
368+
369+__metaclass__ = type
370+__all__ = [
371+ 'PatchedSSHSession',
372+ ]
373+
374+from twisted.conch.ssh import channel, session
375+
376+
377+class PatchedSSHSession(session.SSHSession, object):
378+ """Session adapter that corrects bugs in Conch.
379+
380+ This object provides no custom logic for Launchpad, it just addresses some
381+ simple bugs in the base `session.SSHSession` class that are not yet fixed
382+ upstream.
383+ """
384+
385+ def closeReceived(self):
386+ # Without this, the client hangs when it's finished transferring.
387+ # XXX: JonathanLange 2009-01-05: This does not appear to have a
388+ # corresponding bug in Twisted. We should test that the above comment
389+ # is indeed correct and then file a bug upstream.
390+ self.loseConnection()
391+
392+ def loseConnection(self):
393+ # XXX: JonathanLange 2008-03-31: This deliberately replaces the
394+ # implementation of session.SSHSession.loseConnection. The default
395+ # implementation will try to call loseConnection on the client
396+ # transport even if it's None. I don't know *why* it is None, so this
397+ # doesn't necessarily address the root cause.
398+ # See http://twistedmatrix.com/trac/ticket/2754.
399+ transport = getattr(self.client, 'transport', None)
400+ if transport is not None:
401+ transport.loseConnection()
402+ # This is called by session.SSHSession.loseConnection. SSHChannel is
403+ # the base class of SSHSession.
404+ channel.SSHChannel.loseConnection(self)
405+
406+ def stopWriting(self):
407+ """See `session.SSHSession.stopWriting`.
408+
409+ When the client can't keep up with us, we ask the child process to
410+ stop giving us data.
411+ """
412+ # XXX: MichaelHudson 2008-06-27: Being cagey about whether
413+ # self.client.transport is entirely paranoia inspired by the comment
414+ # in `loseConnection` above. It would be good to know if and why it is
415+ # necessary. See http://twistedmatrix.com/trac/ticket/2754.
416+ transport = getattr(self.client, 'transport', None)
417+ if transport is not None:
418+ # For SFTP connections, 'transport' is actually a _DummyTransport
419+ # instance. Neither _DummyTransport nor the protocol it wraps
420+ # (filetransfer.FileTransferServer) support pausing.
421+ pauseProducing = getattr(transport, 'pauseProducing', None)
422+ if pauseProducing is not None:
423+ pauseProducing()
424+
425+ def startWriting(self):
426+ """See `session.SSHSession.startWriting`.
427+
428+ The client is ready for data again, so ask the child to start
429+ producing data again.
430+ """
431+ # XXX: MichaelHudson 2008-06-27: Being cagey about whether
432+ # self.client.transport is entirely paranoia inspired by the comment
433+ # in `loseConnection` above. It would be good to know if and why it is
434+ # necessary. See http://twistedmatrix.com/trac/ticket/2754.
435+ transport = getattr(self.client, 'transport', None)
436+ if transport is not None:
437+ # For SFTP connections, 'transport' is actually a _DummyTransport
438+ # instance. Neither _DummyTransport nor the protocol it wraps
439+ # (filetransfer.FileTransferServer) support pausing.
440+ resumeProducing = getattr(transport, 'resumeProducing', None)
441+ if resumeProducing is not None:
442+ resumeProducing()
443
444=== added file 'lib/lp/services/sshserver/sftp.py'
445--- lib/lp/services/sshserver/sftp.py 1970-01-01 00:00:00 +0000
446+++ lib/lp/services/sshserver/sftp.py 2010-04-16 19:01:18 +0000
447@@ -0,0 +1,35 @@
448+# Copyright 2010 Canonical Ltd. This software is licensed under the
449+# GNU Affero General Public License version 3 (see the file LICENSE).
450+
451+"""Generic SFTP server functionality."""
452+
453+__metaclass__ = type
454+__all__ = [
455+ 'FileTransferServer',
456+ ]
457+
458+from twisted.conch.ssh import filetransfer
459+
460+from zope.event import notify
461+
462+from lp.services.sshserver import events
463+
464+
465+class FileTransferServer(filetransfer.FileTransferServer):
466+ """SFTP protocol implementation that logs key events."""
467+
468+ def __init__(self, data=None, avatar=None):
469+ filetransfer.FileTransferServer.__init__(self, data, avatar)
470+ notify(events.SFTPStarted(avatar))
471+ self.avatar = avatar
472+
473+ def connectionLost(self, reason):
474+ # This method gets called twice: once from `SSHChannel.closeReceived`
475+ # when the client closes the channel and once from `SSHSession.closed`
476+ # when the server closes the session. We change the avatar attribute
477+ # to avoid logging the `SFTPClosed` event twice.
478+ filetransfer.FileTransferServer.connectionLost(self, reason)
479+ if self.avatar is not None:
480+ avatar = self.avatar
481+ self.avatar = None
482+ notify(events.SFTPClosed(avatar))
483
484=== added directory 'lib/lp/services/sshserver/tests'
485=== added file 'lib/lp/services/sshserver/tests/__init__.py'
486--- lib/lp/services/sshserver/tests/__init__.py 1970-01-01 00:00:00 +0000
487+++ lib/lp/services/sshserver/tests/__init__.py 2010-04-16 19:01:18 +0000
488@@ -0,0 +1,8 @@
489+# Copyright 2010 Canonical Ltd. This software is licensed under the
490+# GNU Affero General Public License version 3 (see the file LICENSE).
491+
492+"""Tests for the Launchpad SSH server."""
493+
494+__metaclass__ = type
495+__all__ = []
496+
497
498=== added directory 'lib/lp/services/sshserver/tests/keys'
499=== added file 'lib/lp/services/sshserver/tests/keys/ssh_host_key_rsa'
500--- lib/lp/services/sshserver/tests/keys/ssh_host_key_rsa 1970-01-01 00:00:00 +0000
501+++ lib/lp/services/sshserver/tests/keys/ssh_host_key_rsa 2010-04-16 19:01:18 +0000
502@@ -0,0 +1,15 @@
503+-----BEGIN RSA PRIVATE KEY-----
504+MIICXAIBAAKBgQDSSpVRPhCiU9PPuZN7QyJdMOgTVwPyYpZGOHutR/9kxFvOLa39
505+nY0Eqo39OTumfZBMEEVqIPadQanO9LcdTnl9/Z4LcBGn09EFQ2y7VUkC6J2dSQtr
506+YMY0tV+C5HGZ2oYBWKBl5PZ1RI4+qrJpAMMmINdnF0uEE/x8B1iMWGB3PwIBIwKB
507+gQCcN2ebb+8ZgBmwQLazVnFMirsHDXClbc630i/9EOmbUAmvGp6B4sCHH5ytevkc
508+l8pHIget7JnxKXbUQMKKzJTCpPwwEyL3ZVDxYXg37WQU74cVf93CjOjChs+hOeS1
509+sW5m9JFr9oomL5JWnGXr+TV/kYBCNVW++J1Bckn6kYpH+wJBAPUw0ZunXlBRuugA
510+YTSmXUUX+ALu6maDD1t7gAk37waxNQMaH5DMk5R4IQtoxeQgCLqL2yEJUqK1lxOy
511+wlp1k8UCQQDbj+Vr4poE9MpYNtPDiDqv2aXe6CJ3p1qQuNE0rSxk+0G9h3ASISRw
512+DDLgcapg2xkOvG3pidfAJG9P827XiVszAkEA4CyiYm0jB5suivkIaqa7rOK23hxE
513+BfQrTFOoQvFPkRcL5ZQ6Hfzes6EIRPIUA8WEUskC3GBLjXLTRTW5Aj+dCwJBAMi+
514+E5XWfjBqx6EcL1OvwKDG/g2hCZH4GEnNjBLneQveZ/29qEsW/L43CfHHAiyrD5hx
515+w5OxOklF4h02VrZu9EsCQBEZcTAQOrWnkmp7uBrz1V8nFLAE6zFh/6Wj1Mn8k08j
516+pcsLJAhm+qlV7EtV/5rk+v3WcXrKiRIiiC0Ron96Dx0=
517+-----END RSA PRIVATE KEY-----
518
519=== added file 'lib/lp/services/sshserver/tests/keys/ssh_host_key_rsa.pub'
520--- lib/lp/services/sshserver/tests/keys/ssh_host_key_rsa.pub 1970-01-01 00:00:00 +0000
521+++ lib/lp/services/sshserver/tests/keys/ssh_host_key_rsa.pub 2010-04-16 19:01:18 +0000
522@@ -0,0 +1,1 @@
523+ssh-rsa AAAAB3NzaC1yc2EAAAABIwAAAIEA0kqVUT4QolPTz7mTe0MiXTDoE1cD8mKWRjh7rUf/ZMRbzi2t/Z2NBKqN/Tk7pn2QTBBFaiD2nUGpzvS3HU55ff2eC3ARp9PRBUNsu1VJAuidnUkLa2DGNLVfguRxmdqGAVigZeT2dUSOPqqyaQDDJiDXZxdLhBP8fAdYjFhgdz8= andrew@frobozz
524
525=== renamed file 'lib/lp/codehosting/sshserver/tests/test_logging.py' => 'lib/lp/services/sshserver/tests/test_accesslog.py'
526--- lib/lp/codehosting/sshserver/tests/test_logging.py 2010-04-16 19:01:12 +0000
527+++ lib/lp/services/sshserver/tests/test_accesslog.py 2010-04-16 19:01:18 +0000
528@@ -18,7 +18,7 @@
529 import zope.component.event
530
531 from canonical.launchpad.scripts import WatchedFileHandler
532-from lp.codehosting.sshserver.accesslog import LoggingManager
533+from lp.services.sshserver.accesslog import LoggingManager
534 from lp.testing import TestCase
535
536
537@@ -71,9 +71,9 @@
538 self.assertEqual(root_handlers, logging.getLogger('').handlers)
539 self.assertEqual(bzr_handlers, logging.getLogger('bzr').handlers)
540
541- def test_codehosting_log_doesnt_go_to_stderr(self):
542+ def test_log_doesnt_go_to_stderr(self):
543 # Once logging setup is called, any messages logged to the
544- # codehosting logger should *not* be logged to stderr. If they are,
545+ # SSH server logger should *not* be logged to stderr. If they are,
546 # they will appear on the user's terminal.
547 log = self.makeLogger()
548 self.installLoggingManager(log)
549@@ -136,7 +136,7 @@
550
551 def test_access_handlers(self):
552 # The logging setup installs a rotatable log handler that logs output
553- # to config.codehosting.access_log.
554+ # to the SSH server access log.
555 directory = self.makeTemporaryDirectory()
556 access_log = self.makeLogger()
557 access_log_path = os.path.join(directory, 'access.log')
558
559=== renamed file 'lib/lp/codehosting/sshserver/tests/test_auth.py' => 'lib/lp/services/sshserver/tests/test_auth.py'
560--- lib/lp/codehosting/sshserver/tests/test_auth.py 2010-04-16 19:01:12 +0000
561+++ lib/lp/services/sshserver/tests/test_auth.py 2010-04-16 19:01:18 +0000
562@@ -21,10 +21,9 @@
563
564 from twisted.trial.unittest import TestCase as TrialTestCase
565
566-from canonical.config import config
567 from canonical.launchpad.xmlrpc import faults
568 from canonical.testing.layers import TwistedLayer
569-from lp.codehosting.sshserver import auth
570+from lp.services.sshserver import auth
571 from lp.services.twistedsupport import suppress_stderr
572
573
574@@ -224,7 +223,7 @@
575
576 def test_bannerNotSentOnSuccess(self):
577 # No banner is printed when the user authenticates successfully.
578- self.assertEqual(None, config.codehosting.banner)
579+ self.user_auth._banner = None
580
581 d = self.requestSuccessfulAuthentication()
582 def check(ignored):
583
584=== renamed file 'lib/lp/codehosting/sshserver/tests/test_events.py' => 'lib/lp/services/sshserver/tests/test_events.py'
585--- lib/lp/codehosting/sshserver/tests/test_events.py 2010-04-16 19:01:12 +0000
586+++ lib/lp/services/sshserver/tests/test_events.py 2010-04-16 19:01:18 +0000
587@@ -13,7 +13,7 @@
588 import zope.component.event
589 from zope.event import notify
590
591-from lp.codehosting.sshserver.events import ILoggingEvent, LoggingEvent
592+from lp.services.sshserver.events import ILoggingEvent, LoggingEvent
593
594 from lp.testing import TestCase
595