Merge lp:~jml/launchpad/buildd-deferred into lp:launchpad
- buildd-deferred
- Merge into devel
Proposed by
Jonathan Lange
Status: | Merged |
---|---|
Approved by: | Julian Edwards |
Approved revision: | no longer in the source branch. |
Merged at revision: | 11581 |
Proposed branch: | lp:~jml/launchpad/buildd-deferred |
Merge into: | lp:launchpad |
Diff against target: |
396 lines (+230/-12) 7 files modified
lib/canonical/buildd/slave.py (+2/-0) lib/canonical/buildd/tests/buildd-slave-test.conf (+7/-0) lib/lp/buildmaster/manager.py (+15/-1) lib/lp/buildmaster/model/builder.py (+45/-9) lib/lp/buildmaster/tests/test_builder.py (+157/-1) lib/lp/buildmaster/tests/test_manager.py (+0/-1) lib/lp/soyuz/tests/soyuzbuilddhelpers.py (+4/-0) |
To merge this branch: | bzr merge lp:~jml/launchpad/buildd-deferred |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Julian Edwards (community) | Approve | ||
Review via email: mp+36010@code.launchpad.net |
Commit message
Change BuilderSlave to use xmlrpclib.
Description of the change
This branch changes the BuilderSlave class in Soyuz to use composition, rather than inheritance, to do the XML-RPC interactions.
To post a comment you must log in.
Revision history for this message
Julian Edwards (julian-edwards) : | # |
review:
Approve
Preview Diff
[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1 | === modified file 'lib/canonical/buildd/slave.py' |
2 | --- lib/canonical/buildd/slave.py 2010-05-20 11:19:56 +0000 |
3 | +++ lib/canonical/buildd/slave.py 2010-09-20 13:43:43 +0000 |
4 | @@ -356,6 +356,8 @@ |
5 | # for abort to complete. This is potentially an issue in a heavy |
6 | # load situation. |
7 | if self.builderstatus != BuilderStatus.BUILDING: |
8 | + # XXX: Should raise a known Fault so that the client can make |
9 | + # useful decisions about the error! |
10 | raise ValueError("Slave is not BUILDING when asked to abort") |
11 | self.manager.abort() |
12 | self.builderstatus = BuilderStatus.ABORTING |
13 | |
14 | === modified file 'lib/canonical/buildd/tests/buildd-slave-test.conf' |
15 | --- lib/canonical/buildd/tests/buildd-slave-test.conf 2010-06-12 03:48:34 +0000 |
16 | +++ lib/canonical/buildd/tests/buildd-slave-test.conf 2010-09-20 13:43:43 +0000 |
17 | @@ -18,3 +18,10 @@ |
18 | updatepath = /var/tmp/buildd/slavebin/update-debian-chroot |
19 | processscanpath = /var/tmp/buildd/slavebin/scan-for-processes |
20 | sourcespath = /usr/share/launchpad-buildd/slavebin/override-sources-list |
21 | + |
22 | +[binarypackagemanager] |
23 | +sbuildpath = /var/tmp/buildd/slavebin/sbuild-package |
24 | +sbuildargs = -dautobuild --nolog --batch |
25 | +updatepath = /var/tmp/buildd/slavebin/update-debian-chroot |
26 | +processscanpath = /var/tmp/buildd/slavebin/scan-for-processes |
27 | +sourcespath = /usr/share/launchpad-buildd/slavebin/override-sources-list |
28 | |
29 | === modified file 'lib/lp/buildmaster/manager.py' |
30 | --- lib/lp/buildmaster/manager.py 2010-09-02 11:35:36 +0000 |
31 | +++ lib/lp/buildmaster/manager.py 2010-09-20 13:43:43 +0000 |
32 | @@ -106,6 +106,7 @@ |
33 | |
34 | def build(self, *args): |
35 | """Perform the build.""" |
36 | + # XXX: This method does not appear to be used. |
37 | self.calls.append(('build', args)) |
38 | result = buildd_success_result_map.get('build') |
39 | return [result, args[0]] |
40 | @@ -275,6 +276,7 @@ |
41 | def scheduleNextScanCycle(self): |
42 | """Schedule another scan of the builder some time in the future.""" |
43 | self._deferred_list = [] |
44 | + # XXX: Change this to use LoopingCall. |
45 | reactor.callLater(self.SCAN_INTERVAL, self.startCycle) |
46 | |
47 | def startCycle(self): |
48 | @@ -286,6 +288,7 @@ |
49 | if slave is None: |
50 | self.scheduleNextScanCycle() |
51 | else: |
52 | + # XXX: Ought to return Deferred. |
53 | self.resumeAndDispatch(slave) |
54 | except: |
55 | error = Failure() |
56 | @@ -362,9 +365,16 @@ |
57 | return None |
58 | |
59 | # See if there is a job we can dispatch to the builder slave. |
60 | + |
61 | + # XXX: Rather than use the slave actually associated with the builder |
62 | + # (which, incidentally, shouldn't be a property anyway), we make a new |
63 | + # RecordingSlave so we can get access to its asynchronous |
64 | + # "resumeSlave" method. Blech. |
65 | slave = RecordingSlave( |
66 | self.builder.name, self.builder.url, self.builder.vm_host) |
67 | - candidate = self.builder.findAndStartJob(buildd_slave=slave) |
68 | + # XXX: Passing buildd_slave=slave overwrites the 'slave' property of |
69 | + # self.builder. Not sure why this is needed yet. |
70 | + self.builder.findAndStartJob(buildd_slave=slave) |
71 | if self.builder.currentjob is not None: |
72 | # After a successful dispatch we can reset the |
73 | # failure_count. |
74 | @@ -376,9 +386,13 @@ |
75 | |
76 | def resumeAndDispatch(self, slave): |
77 | """Chain the resume and dispatching Deferreds.""" |
78 | + # XXX: resumeAndDispatch makes Deferreds without returning them. |
79 | if slave.resume_requested: |
80 | # The slave needs to be reset before we can dispatch to |
81 | # it (e.g. a virtual slave) |
82 | + |
83 | + # XXX: Two problems here. The first is that 'resumeSlave' only |
84 | + # exists on RecordingSlave (BuilderSlave calls it 'resume'). |
85 | d = slave.resumeSlave() |
86 | d.addBoth(self.checkResume, slave) |
87 | else: |
88 | |
89 | === modified file 'lib/lp/buildmaster/model/builder.py' |
90 | --- lib/lp/buildmaster/model/builder.py 2010-09-03 15:02:39 +0000 |
91 | +++ lib/lp/buildmaster/model/builder.py 2010-09-20 13:43:43 +0000 |
92 | @@ -112,17 +112,52 @@ |
93 | return TimeoutHTTP(host) |
94 | |
95 | |
96 | -class BuilderSlave(xmlrpclib.ServerProxy): |
97 | +class BuilderSlave(object): |
98 | """Add in a few useful methods for the XMLRPC slave.""" |
99 | |
100 | + # XXX: This (BuilderSlave) should use composition, rather than |
101 | + # inheritance. |
102 | + |
103 | + # XXX: Have a documented interface for the XML-RPC server: |
104 | + # - what methods |
105 | + # - what return values expected |
106 | + # - what faults |
107 | + # (see XMLRPCBuildDSlave in lib/canonical/buildd/slave.py). |
108 | + |
109 | + # XXX: Arguably, this interface should be asynchronous |
110 | + # (i.e. Deferred-returning). This would mean that Builder (see below) |
111 | + # would have to expect Deferreds. |
112 | + |
113 | + # XXX: Once we have a client object with a defined, tested interface, we |
114 | + # should make a test double that doesn't do any XML-RPC and can be used to |
115 | + # make testing easier & tests faster. |
116 | + |
117 | def __init__(self, urlbase, vm_host): |
118 | """Initialise a Server with specific parameter to our buildfarm.""" |
119 | self.vm_host = vm_host |
120 | self.urlbase = urlbase |
121 | rpc_url = urlappend(urlbase, "rpc") |
122 | - xmlrpclib.Server.__init__(self, rpc_url, |
123 | - transport=TimeoutTransport(), |
124 | - allow_none=True) |
125 | + self._server = xmlrpclib.Server( |
126 | + rpc_url, transport=TimeoutTransport(), allow_none=True) |
127 | + |
128 | + def abort(self): |
129 | + return self._server.abort() |
130 | + |
131 | + def echo(self, *args): |
132 | + """Echo the arguments back.""" |
133 | + return self._server.echo(*args) |
134 | + |
135 | + def info(self): |
136 | + """Return the protocol version and the builder methods supported.""" |
137 | + return self._server.info() |
138 | + |
139 | + def status(self): |
140 | + """Return the status of the build daemon.""" |
141 | + return self._server.status() |
142 | + |
143 | + def ensurepresent(self, sha1sum, url, username, password): |
144 | + """Attempt to ensure the given file is present.""" |
145 | + return self._server.ensurepresent(sha1sum, url, username, password) |
146 | |
147 | def getFile(self, sha_sum): |
148 | """Construct a file-like object to return the named file.""" |
149 | @@ -138,6 +173,10 @@ |
150 | |
151 | :return: a (stdout, stderr, subprocess exitcode) triple |
152 | """ |
153 | + # XXX: This executes the vm_resume_command |
154 | + # synchronously. RecordingSlave does so asynchronously. Since we |
155 | + # always want to do this asynchronously, there's no need for the |
156 | + # duplication. |
157 | resume_command = config.builddmaster.vm_resume_command % { |
158 | 'vm_host': self.vm_host} |
159 | resume_argv = resume_command.split() |
160 | @@ -176,12 +215,9 @@ |
161 | :param args: A dictionary of extra arguments. The contents depend on |
162 | the build job type. |
163 | """ |
164 | - # Can't upcall to xmlrpclib.ServerProxy, since it doesn't actually |
165 | - # have a 'build' method. |
166 | - build_method = xmlrpclib.ServerProxy.__getattr__(self, 'build') |
167 | try: |
168 | - return build_method( |
169 | - self, buildid, builder_type, chroot_sha1, filemap, args) |
170 | + return self._server.build( |
171 | + buildid, builder_type, chroot_sha1, filemap, args) |
172 | except xmlrpclib.Fault, info: |
173 | raise BuildSlaveFailure(info) |
174 | |
175 | |
176 | === modified file 'lib/lp/buildmaster/tests/test_builder.py' |
177 | --- lib/lp/buildmaster/tests/test_builder.py 2010-08-30 15:00:23 +0000 |
178 | +++ lib/lp/buildmaster/tests/test_builder.py 2010-09-20 13:43:43 +0000 |
179 | @@ -4,11 +4,18 @@ |
180 | """Test Builder features.""" |
181 | |
182 | import errno |
183 | +import os |
184 | import socket |
185 | +import xmlrpclib |
186 | + |
187 | +from testtools.content import Content |
188 | +from testtools.content_type import UTF8_TEXT |
189 | |
190 | from zope.component import getUtility |
191 | from zope.security.proxy import removeSecurityProxy |
192 | |
193 | +from canonical.buildd.slave import BuilderStatus |
194 | +from canonical.buildd.tests.harness import BuilddSlaveTestSetup |
195 | from canonical.database.sqlbase import flush_database_updates |
196 | from canonical.launchpad.webapp.interfaces import ( |
197 | DEFAULT_FLAVOR, |
198 | @@ -25,6 +32,7 @@ |
199 | IBuildFarmJobBehavior, |
200 | ) |
201 | from lp.buildmaster.interfaces.buildqueue import IBuildQueueSet |
202 | +from lp.buildmaster.model.builder import BuilderSlave |
203 | from lp.buildmaster.model.buildfarmjobbehavior import IdleBuildBehavior |
204 | from lp.buildmaster.model.buildqueue import BuildQueue |
205 | from lp.soyuz.enums import ( |
206 | @@ -40,7 +48,10 @@ |
207 | MockBuilder, |
208 | ) |
209 | from lp.soyuz.tests.test_publishing import SoyuzTestPublisher |
210 | -from lp.testing import TestCaseWithFactory |
211 | +from lp.testing import ( |
212 | + TestCase, |
213 | + TestCaseWithFactory, |
214 | + ) |
215 | from lp.testing.fakemethod import FakeMethod |
216 | |
217 | |
218 | @@ -454,3 +465,148 @@ |
219 | |
220 | self.assertIsInstance( |
221 | self.builder.current_build_behavior, BinaryPackageBuildBehavior) |
222 | + |
223 | + |
224 | +class TestSlave(TestCase): |
225 | + """ |
226 | + Integration tests for BuilderSlave that verify how it works against a |
227 | + real slave server. |
228 | + """ |
229 | + |
230 | + # XXX: JonathanLange 2010-09-20 bug=643521: There are also tests for |
231 | + # BuilderSlave in buildd-slave.txt and in other places. The tests here |
232 | + # ought to become the canonical tests for BuilderSlave vs running buildd |
233 | + # XML-RPC server interaction. |
234 | + |
235 | + # The URL for the XML-RPC service set up by `BuilddSlaveTestSetup`. |
236 | + TEST_URL = 'http://localhost:8221/rpc/' |
237 | + |
238 | + def getServerSlave(self): |
239 | + """Set up a test build slave server. |
240 | + |
241 | + :return: A `BuilddSlaveTestSetup` object. |
242 | + """ |
243 | + tachandler = BuilddSlaveTestSetup() |
244 | + tachandler.setUp() |
245 | + def addLogFile(exc_info): |
246 | + self.addDetail( |
247 | + 'xmlrpc-log-file', |
248 | + Content(UTF8_TEXT, lambda: open(tachandler.logfile, 'r').read())) |
249 | + self.addOnException(addLogFile) |
250 | + self.addCleanup(tachandler.tearDown) |
251 | + return tachandler |
252 | + |
253 | + def getClientSlave(self): |
254 | + """Return a `BuilderSlave` for use in testing. |
255 | + |
256 | + Points to a fixed URL that is also used by `BuilddSlaveTestSetup`. |
257 | + """ |
258 | + return BuilderSlave(self.TEST_URL, 'vmhost') |
259 | + |
260 | + def makeCacheFile(self, tachandler, filename): |
261 | + """Make a cache file available on the remote slave. |
262 | + |
263 | + :param tachandler: The TacTestSetup object used to start the remote |
264 | + slave. |
265 | + :param filename: The name of the file to create in the file cache |
266 | + area. |
267 | + """ |
268 | + path = os.path.join(tachandler.root, 'filecache', filename) |
269 | + fd = open(path, 'w') |
270 | + fd.write('something') |
271 | + fd.close() |
272 | + self.addCleanup(os.unlink, path) |
273 | + |
274 | + def triggerGoodBuild(self, slave, build_id=None): |
275 | + """Trigger a good build on 'slave'. |
276 | + |
277 | + :param slave: A `BuilderSlave` instance to trigger the build on. |
278 | + :param build_id: The build identifier. If not specified, defaults to |
279 | + an arbitrary string. |
280 | + :type build_id: str |
281 | + :return: The build id returned by the slave. |
282 | + """ |
283 | + if build_id is None: |
284 | + build_id = self.getUniqueString() |
285 | + tachandler = self.getServerSlave() |
286 | + chroot_file = 'fake-chroot' |
287 | + dsc_file = 'thing' |
288 | + self.makeCacheFile(tachandler, chroot_file) |
289 | + self.makeCacheFile(tachandler, dsc_file) |
290 | + return slave.build( |
291 | + build_id, 'debian', chroot_file, {'.dsc': dsc_file}, |
292 | + {'ogrecomponent': 'main'}) |
293 | + |
294 | + def test_abort(self): |
295 | + slave = self.getClientSlave() |
296 | + # We need to be in a BUILDING state before we can abort. |
297 | + self.triggerGoodBuild(slave) |
298 | + result = slave.abort() |
299 | + self.assertEqual(result, BuilderStatus.ABORTING) |
300 | + |
301 | + def test_build(self): |
302 | + # Calling 'build' with an expected builder type, a good build id, |
303 | + # valid chroot & filemaps works and returns a BuilderStatus of |
304 | + # BUILDING. |
305 | + build_id = 'some-id' |
306 | + slave = self.getClientSlave() |
307 | + result = self.triggerGoodBuild(slave, build_id) |
308 | + self.assertEqual([BuilderStatus.BUILDING, build_id], result) |
309 | + |
310 | + def test_echo(self): |
311 | + # Calling 'echo' contacts the server which returns the arguments we |
312 | + # gave it. |
313 | + self.getServerSlave() |
314 | + slave = self.getClientSlave() |
315 | + result = slave.echo('foo', 'bar', 42) |
316 | + self.assertEqual(['foo', 'bar', 42], result) |
317 | + |
318 | + def test_info(self): |
319 | + # Calling 'info' gets some information about the slave. |
320 | + self.getServerSlave() |
321 | + slave = self.getClientSlave() |
322 | + result = slave.info() |
323 | + # We're testing the hard-coded values, since the version is hard-coded |
324 | + # into the remote slave, the supported build managers are hard-coded |
325 | + # into the tac file for the remote slave and config is returned from |
326 | + # the configuration file. |
327 | + self.assertEqual( |
328 | + ['1.0', |
329 | + 'i386', |
330 | + ['sourcepackagerecipe', |
331 | + 'translation-templates', 'binarypackage', 'debian']], |
332 | + result) |
333 | + |
334 | + def test_initial_status(self): |
335 | + # Calling 'status' returns the current status of the slave. The |
336 | + # initial status is IDLE. |
337 | + self.getServerSlave() |
338 | + slave = self.getClientSlave() |
339 | + status = slave.status() |
340 | + self.assertEqual([BuilderStatus.IDLE, ''], status) |
341 | + |
342 | + def test_status_after_build(self): |
343 | + # Calling 'status' returns the current status of the slave. After a |
344 | + # build has been triggered, the status is BUILDING. |
345 | + slave = self.getClientSlave() |
346 | + build_id = 'status-build-id' |
347 | + self.triggerGoodBuild(slave, build_id) |
348 | + status = slave.status() |
349 | + self.assertEqual([BuilderStatus.BUILDING, build_id], status[:2]) |
350 | + [log_file] = status[2:] |
351 | + self.assertIsInstance(log_file, xmlrpclib.Binary) |
352 | + |
353 | + def test_ensurepresent_not_there(self): |
354 | + # ensurepresent checks to see if a file is there. |
355 | + self.getServerSlave() |
356 | + slave = self.getClientSlave() |
357 | + result = slave.ensurepresent('blahblah', None, None, None) |
358 | + self.assertEqual([False, 'No URL'], result) |
359 | + |
360 | + def test_ensurepresent_actually_there(self): |
361 | + # ensurepresent checks to see if a file is there. |
362 | + tachandler = self.getServerSlave() |
363 | + slave = self.getClientSlave() |
364 | + self.makeCacheFile(tachandler, 'blahblah') |
365 | + result = slave.ensurepresent('blahblah', None, None, None) |
366 | + self.assertEqual([True, 'No URL'], result) |
367 | |
368 | === modified file 'lib/lp/buildmaster/tests/test_manager.py' |
369 | --- lib/lp/buildmaster/tests/test_manager.py 2010-08-30 15:00:23 +0000 |
370 | +++ lib/lp/buildmaster/tests/test_manager.py 2010-09-20 13:43:43 +0000 |
371 | @@ -6,7 +6,6 @@ |
372 | import os |
373 | import signal |
374 | import time |
375 | -import unittest |
376 | |
377 | import transaction |
378 | |
379 | |
380 | === modified file 'lib/lp/soyuz/tests/soyuzbuilddhelpers.py' |
381 | --- lib/lp/soyuz/tests/soyuzbuilddhelpers.py 2010-08-20 20:31:18 +0000 |
382 | +++ lib/lp/soyuz/tests/soyuzbuilddhelpers.py 2010-09-20 13:43:43 +0000 |
383 | @@ -61,10 +61,14 @@ |
384 | slave_build_id) |
385 | |
386 | def cleanSlave(self): |
387 | + # XXX: This should not print anything. The print is only here to make |
388 | + # doc/builder.txt a meaningful test. |
389 | print 'Cleaning slave' |
390 | return self.slave.clean() |
391 | |
392 | def requestAbort(self): |
393 | + # XXX: This should not print anything. The print is only here to make |
394 | + # doc/builder.txt a meaningful test. |
395 | print 'Aborting slave' |
396 | return self.slave.abort() |
397 |