Merge lp:~michael.nelson/launchpad/567922-binarypackagebuild-new-table-5 into lp:launchpad/db-devel
- 567922-binarypackagebuild-new-table-5
- Merge into db-devel
Status: | Merged | ||||
---|---|---|---|---|---|
Approved by: | Michael Nelson | ||||
Approved revision: | no longer in the source branch. | ||||
Merged at revision: | 9405 | ||||
Proposed branch: | lp:~michael.nelson/launchpad/567922-binarypackagebuild-new-table-5 | ||||
Merge into: | lp:launchpad/db-devel | ||||
Prerequisite: | lp:~michael.nelson/launchpad/567922-binarypackagebuild-new-table-4 | ||||
Diff against target: |
970 lines (+248/-179) (has conflicts) 10 files modified
lib/canonical/launchpad/security.py (+45/-27) lib/lp/buildmaster/configure.zcml (+19/-12) lib/lp/buildmaster/model/buildfarmjob.py (+5/-3) lib/lp/buildmaster/model/packagebuild.py (+8/-2) lib/lp/buildmaster/tests/test_buildfarmjob.py (+38/-6) lib/lp/buildmaster/tests/test_packagebuild.py (+43/-1) lib/lp/soyuz/doc/binarypackagebuild.txt (+50/-104) lib/lp/soyuz/doc/buildd-mass-retry.txt (+1/-1) lib/lp/soyuz/model/binarypackagebuild.py (+38/-22) lib/lp/soyuz/stories/webservice/xx-builds.txt (+1/-1) Text conflict in lib/lp/buildmaster/interfaces/buildbase.py Text conflict in lib/lp/buildmaster/model/buildbase.py Text conflict in lib/lp/buildmaster/tests/test_buildbase.py Text conflict in lib/lp/code/model/sourcepackagerecipebuild.py |
||||
To merge this branch: | bzr merge lp:~michael.nelson/launchpad/567922-binarypackagebuild-new-table-5 | ||||
Related bugs: |
|
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Eleanor Berger (community) | code | Approve | |
Review via email: mp+25239@code.launchpad.net |
Commit message
Description of the change
This branch is part of a pipeline for
https:/
https:/
**Note**: If it's possible, please ignore the conflicts with db-devel - it's due to a reversion of some work that was in db-devel and that I'd already pumped through the pipeline, and I'm waiting for that work to land again on db-devel before re-merging and pumping.
The actual diff of this branch from the previous is:
http://
Overview
========
This branch continues the work to switch our BinaryPackageBuild class to the new binarypackagebuild table (using the delegated PackageBuild/
This branch gets just renames build.txt => binarypackagebu
Note: Some parts of the doctest were removed in favour of new unit tests.
This branch is dependent on the pending schema patch in a previous branch.
To test
=======
First update the test db schema (required as the db patch still needs to be updated to remove the old build table):
psql launchpad_
bin/py database/
And then:
bin/test -vv -t test_buildfarmjob -t test_packagebuild -t doc/binarypacka
The next branch will continue getting the remaining soyuz doctests passing with the new model.
Eleanor Berger (intellectronica) : | # |
Preview Diff
1 | === modified file 'lib/canonical/launchpad/security.py' |
2 | --- lib/canonical/launchpad/security.py 2010-05-13 15:56:00 +0000 |
3 | +++ lib/canonical/launchpad/security.py 2010-05-13 15:56:21 +0000 |
4 | @@ -39,7 +39,9 @@ |
5 | from lp.bugs.interfaces.bugtracker import IBugTracker |
6 | from lp.buildmaster.interfaces.builder import IBuilder, IBuilderSet |
7 | from lp.buildmaster.interfaces.buildfarmbranchjob import IBuildFarmBranchJob |
8 | -from lp.buildmaster.interfaces.buildfarmjob import IBuildFarmJob |
9 | +from lp.buildmaster.interfaces.buildfarmjob import ( |
10 | + IBuildFarmJob, IBuildFarmJobOld) |
11 | +from lp.buildmaster.interfaces.packagebuild import IPackageBuild |
12 | from lp.code.interfaces.codeimport import ICodeImport |
13 | from lp.code.interfaces.codeimportjob import ( |
14 | ICodeImportJobSet, ICodeImportJobWorkflow) |
15 | @@ -1446,10 +1448,29 @@ |
16 | |
17 | |
18 | class AdminBuildRecord(AdminByBuilddAdmin): |
19 | - usedfor = IBinaryPackageBuild |
20 | - |
21 | - |
22 | -class EditBuildRecord(AdminByBuilddAdmin): |
23 | + usedfor = IBuildFarmJob |
24 | + |
25 | + |
26 | +class EditBuildFarmJob(AdminByBuilddAdmin): |
27 | + permission = 'launchpad.Edit' |
28 | + usedfor = IBuildFarmJob |
29 | + |
30 | + |
31 | +class EditPackageBuild(EditBuildFarmJob): |
32 | + usedfor = IPackageBuild |
33 | + |
34 | + def checkAuthenticated(self, user): |
35 | + """Check if the user has access to edit the archive.""" |
36 | + if EditBuildFarmJob.checkAuthenticated(self, user): |
37 | + return True |
38 | + |
39 | + # If the user is in the owning team for the archive, |
40 | + # then they have access to edit the builds. |
41 | + # If it's a PPA or a copy archive only allow its owner. |
42 | + return (self.obj.archive.owner and |
43 | + user.inTeam(self.obj.archive.owner)) |
44 | + |
45 | +class EditBinaryPackageBuild(EditPackageBuild): |
46 | permission = 'launchpad.Edit' |
47 | usedfor = IBinaryPackageBuild |
48 | |
49 | @@ -1462,29 +1483,26 @@ |
50 | * users with upload permissions (for the respective distribution) |
51 | otherwise. |
52 | """ |
53 | - if AdminByBuilddAdmin.checkAuthenticated(self, user): |
54 | + if EditPackageBuild.checkAuthenticated(self, user): |
55 | return True |
56 | |
57 | - # If it's a PPA or a copy archive only allow its owner. |
58 | - if self.obj.archive.is_ppa or self.obj.archive.is_copy: |
59 | - return (self.obj.archive.owner and |
60 | - user.inTeam(self.obj.archive.owner)) |
61 | - |
62 | # Primary or partner section here: is the user in question allowed |
63 | # to upload to the respective component, packageset or package? Allow |
64 | # user to retry build if so. |
65 | # strict_component is True because the source package already exists, |
66 | # otherwise, how can they give it back? |
67 | - check_perms = check_upload_to_archive( |
68 | - user.person, self.obj.distro_series, |
69 | - self.obj.source_package_release.sourcepackagename, |
70 | - self.obj.archive, |
71 | - self.obj.current_component, self.obj.pocket, |
72 | - strict_component=True) |
73 | - return check_perms == None |
74 | - |
75 | - |
76 | -class ViewBuildRecord(EditBuildRecord): |
77 | + if self.obj.archive.is_main: |
78 | + check_perms = check_upload_to_archive( |
79 | + user.person, self.obj.distro_series, |
80 | + self.obj.source_package_release.sourcepackagename, |
81 | + self.obj.archive, |
82 | + self.obj.current_component, self.obj.pocket, |
83 | + strict_component=True) |
84 | + return check_perms == None |
85 | + return False |
86 | + |
87 | + |
88 | +class ViewBinaryPackageBuild(EditBinaryPackageBuild): |
89 | permission = 'launchpad.View' |
90 | |
91 | # This code MUST match the logic in |
92 | @@ -1509,7 +1527,7 @@ |
93 | # privacy since the source package is published publicly. |
94 | # This happens when copy-package is used to re-publish a private |
95 | # package in the primary archive. |
96 | - auth_spr = ViewSourcePackageRelease(self.obj.sourcepackagerelease) |
97 | + auth_spr = ViewSourcePackageRelease(self.obj.source_package_release) |
98 | if auth_spr.checkAuthenticated(user): |
99 | return True |
100 | |
101 | @@ -1522,18 +1540,18 @@ |
102 | return True |
103 | |
104 | # See comment above. |
105 | - auth_spr = ViewSourcePackageRelease(self.obj.sourcepackagerelease) |
106 | + auth_spr = ViewSourcePackageRelease(self.obj.source_package_release) |
107 | return auth_spr.checkUnauthenticated() |
108 | |
109 | |
110 | -class ViewBuildFarmJob(AuthorizationBase): |
111 | - """Permission to view an `IBuildFarmJob`. |
112 | +class ViewBuildFarmJobOld(AuthorizationBase): |
113 | + """Permission to view an `IBuildFarmJobOld`. |
114 | |
115 | This permission is based entirely on permission to view the |
116 | associated `IBinaryPackageBuild` and/or `IBranch`. |
117 | """ |
118 | permission = 'launchpad.View' |
119 | - usedfor = IBuildFarmJob |
120 | + usedfor = IBuildFarmJobOld |
121 | |
122 | def _getBranch(self): |
123 | """Get `IBranch` associated with this job, if any.""" |
124 | @@ -1551,7 +1569,7 @@ |
125 | |
126 | def _checkBuildPermission(self, user=None): |
127 | """Check access to `IBuildBase` for this job.""" |
128 | - permission = ViewBuildRecord(self.obj.build) |
129 | + permission = ViewBinaryPackageBuild(self.obj.build) |
130 | if user is None: |
131 | return permission.checkUnauthenticated() |
132 | else: |
133 | |
134 | === modified file 'lib/lp/buildmaster/configure.zcml' |
135 | --- lib/lp/buildmaster/configure.zcml 2010-05-13 15:56:00 +0000 |
136 | +++ lib/lp/buildmaster/configure.zcml 2010-05-13 15:56:21 +0000 |
137 | @@ -47,6 +47,9 @@ |
138 | class="lp.buildmaster.model.buildfarmjob.BuildFarmJob"> |
139 | <allow |
140 | interface="lp.buildmaster.interfaces.buildfarmjob.IBuildFarmJob" /> |
141 | + <require |
142 | + permission="launchpad.Edit" |
143 | + set_attributes="status"/> |
144 | </class> |
145 | <securedutility |
146 | component="lp.buildmaster.model.buildfarmjob.BuildFarmJob" |
147 | @@ -56,6 +59,22 @@ |
148 | interface="lp.buildmaster.interfaces.buildfarmjob.IBuildFarmJobSource" /> |
149 | </securedutility> |
150 | |
151 | + <!-- PackageBuild --> |
152 | + <class |
153 | + class="lp.buildmaster.model.packagebuild.PackageBuild"> |
154 | + <allow |
155 | + interface="lp.buildmaster.interfaces.packagebuild.IPackageBuild" /> |
156 | + <require |
157 | + permission="launchpad.Edit" |
158 | + set_attributes="dependencies"/> |
159 | + </class> |
160 | + |
161 | + <securedutility |
162 | + component="lp.buildmaster.model.packagebuild.PackageBuild" |
163 | + provides="lp.buildmaster.interfaces.packagebuild.IPackageBuildSource"> |
164 | + <allow |
165 | + interface="lp.buildmaster.interfaces.packagebuild.IPackageBuildSource" /> |
166 | + </securedutility> |
167 | |
168 | <!-- BuildQueue --> |
169 | <class |
170 | @@ -80,17 +99,5 @@ |
171 | interface="lp.buildmaster.interfaces.buildqueue.IBuildQueueSet"/> |
172 | </securedutility> |
173 | |
174 | - <class |
175 | - class="lp.buildmaster.model.packagebuild.PackageBuild"> |
176 | - <allow |
177 | - interface="lp.buildmaster.interfaces.packagebuild.IPackageBuild" /> |
178 | - </class> |
179 | - |
180 | - <securedutility |
181 | - component="lp.buildmaster.model.packagebuild.PackageBuild" |
182 | - provides="lp.buildmaster.interfaces.packagebuild.IPackageBuildSource"> |
183 | - <allow |
184 | - interface="lp.buildmaster.interfaces.packagebuild.IPackageBuildSource" /> |
185 | - </securedutility> |
186 | |
187 | </configure> |
188 | |
189 | === modified file 'lib/lp/buildmaster/model/buildfarmjob.py' |
190 | --- lib/lp/buildmaster/model/buildfarmjob.py 2010-05-13 15:56:00 +0000 |
191 | +++ lib/lp/buildmaster/model/buildfarmjob.py 2010-05-13 15:56:21 +0000 |
192 | @@ -195,7 +195,7 @@ |
193 | name='job_type', allow_none=False, enum=BuildFarmJobType) |
194 | |
195 | def __init__(self, job_type, status=BuildStatus.NEEDSBUILD, |
196 | - processor=None, virtualized=None): |
197 | + processor=None, virtualized=None, date_created=None): |
198 | super(BuildFarmJob, self).__init__() |
199 | self.job_type, self.status, self.processor, self.virtualized = ( |
200 | job_type, |
201 | @@ -203,13 +203,15 @@ |
202 | processor, |
203 | virtualized, |
204 | ) |
205 | + if date_created is not None: |
206 | + self.date_created = date_created |
207 | |
208 | @classmethod |
209 | def new(cls, job_type, status=BuildStatus.NEEDSBUILD, processor=None, |
210 | - virtualized=None): |
211 | + virtualized=None, date_created=None): |
212 | """See `IBuildFarmJobSource`.""" |
213 | build_farm_job = BuildFarmJob( |
214 | - job_type, status, processor, virtualized) |
215 | + job_type, status, processor, virtualized, date_created) |
216 | |
217 | store = IMasterStore(BuildFarmJob) |
218 | store.add(build_farm_job) |
219 | |
220 | === modified file 'lib/lp/buildmaster/model/packagebuild.py' |
221 | --- lib/lp/buildmaster/model/packagebuild.py 2010-05-13 15:56:00 +0000 |
222 | +++ lib/lp/buildmaster/model/packagebuild.py 2010-05-13 15:56:21 +0000 |
223 | @@ -75,14 +75,15 @@ |
224 | |
225 | @classmethod |
226 | def new(cls, job_type, virtualized, archive, pocket, |
227 | - processor=None, status=BuildStatus.NEEDSBUILD, dependencies=None): |
228 | + processor=None, status=BuildStatus.NEEDSBUILD, dependencies=None, |
229 | + date_created=None): |
230 | """See `IPackageBuildSource`.""" |
231 | store = IMasterStore(PackageBuild) |
232 | |
233 | # Create the BuildFarmJob to which the new PackageBuild |
234 | # will delegate. |
235 | build_farm_job = getUtility(IBuildFarmJobSource).new( |
236 | - job_type, status, processor, virtualized) |
237 | + job_type, status, processor, virtualized, date_created) |
238 | |
239 | package_build = cls(build_farm_job, archive, pocket, dependencies) |
240 | store.add(package_build) |
241 | @@ -107,6 +108,11 @@ |
242 | return None |
243 | return ProxiedLibraryFileAlias(self.log, self).http_url |
244 | |
245 | + @property |
246 | + def is_private(self): |
247 | + """See `IBuildBase`""" |
248 | + return self.archive.private |
249 | + |
250 | def getUploadDirLeaf(self, build_cookie, now=None): |
251 | """See `IPackageBuild`.""" |
252 | return BuildBase.getUploadDirLeaf(build_cookie, now) |
253 | |
254 | === modified file 'lib/lp/buildmaster/tests/test_buildfarmjob.py' |
255 | --- lib/lp/buildmaster/tests/test_buildfarmjob.py 2010-05-13 15:56:00 +0000 |
256 | +++ lib/lp/buildmaster/tests/test_buildfarmjob.py 2010-05-13 15:56:21 +0000 |
257 | @@ -11,6 +11,7 @@ |
258 | |
259 | from storm.store import Store |
260 | from zope.component import getUtility |
261 | +from zope.security.interfaces import Unauthorized |
262 | from zope.security.proxy import removeSecurityProxy |
263 | |
264 | from canonical.database.sqlbase import flush_database_updates |
265 | @@ -20,23 +21,26 @@ |
266 | from lp.buildmaster.interfaces.buildfarmjob import ( |
267 | BuildFarmJobType, IBuildFarmJob, IBuildFarmJobSource) |
268 | from lp.buildmaster.model.buildfarmjob import BuildFarmJob |
269 | -from lp.testing import TestCaseWithFactory |
270 | - |
271 | - |
272 | -class TestBuildFarmJob(TestCaseWithFactory): |
273 | - """Tests for the build farm job object.""" |
274 | +from lp.testing import login, TestCaseWithFactory |
275 | + |
276 | + |
277 | +class TestBuildFarmJobBase(TestCaseWithFactory): |
278 | |
279 | layer = DatabaseFunctionalLayer |
280 | |
281 | def setUp(self): |
282 | """Create a build farm job with which to test.""" |
283 | - super(TestBuildFarmJob, self).setUp() |
284 | + super(TestBuildFarmJobBase, self).setUp() |
285 | self.build_farm_job = self.makeBuildFarmJob() |
286 | |
287 | def makeBuildFarmJob(self): |
288 | return getUtility(IBuildFarmJobSource).new( |
289 | job_type=BuildFarmJobType.PACKAGEBUILD) |
290 | |
291 | + |
292 | +class TestBuildFarmJob(TestBuildFarmJobBase): |
293 | + """Tests for the build farm job object.""" |
294 | + |
295 | def test_providesInterface(self): |
296 | # BuildFarmJob provides IBuildFarmJob |
297 | self.assertProvides(self.build_farm_job, IBuildFarmJob) |
298 | @@ -136,6 +140,34 @@ |
299 | naked_bfj.date_finished = now + duration |
300 | self.failUnlessEqual(duration, self.build_farm_job.duration) |
301 | |
302 | + def test_date_created(self): |
303 | + # date_created can be passed optionally when creating a |
304 | + # bulid farm job to ensure we don't get identical timestamps |
305 | + # when transactions are committed. |
306 | + ten_years_ago = datetime.now(pytz.UTC) - timedelta(365*10) |
307 | + build_farm_job = getUtility(IBuildFarmJobSource).new( |
308 | + job_type=BuildFarmJobType.PACKAGEBUILD, |
309 | + date_created=ten_years_ago) |
310 | + self.failUnlessEqual(ten_years_ago, build_farm_job.date_created) |
311 | + |
312 | + |
313 | +class TestBuildFarmJobSecurity(TestBuildFarmJobBase): |
314 | + |
315 | + def test_view_build_farm_job(self): |
316 | + # Anonymous access can read public builds, but not edit. |
317 | + self.failUnlessEqual( |
318 | + BuildStatus.NEEDSBUILD, self.build_farm_job.status) |
319 | + self.assertRaises( |
320 | + Unauthorized, setattr, self.build_farm_job, |
321 | + 'status', BuildStatus.FULLYBUILT) |
322 | + |
323 | + def test_edit_build_farm_job(self): |
324 | + # Users with edit access can update attributes. |
325 | + login('admin@canonical.com') |
326 | + self.build_farm_job.status = BuildStatus.FULLYBUILT |
327 | + self.failUnlessEqual( |
328 | + BuildStatus.FULLYBUILT, self.build_farm_job.status) |
329 | + |
330 | |
331 | def test_suite(): |
332 | return unittest.TestLoader().loadTestsFromName(__name__) |
333 | |
334 | === modified file 'lib/lp/buildmaster/tests/test_packagebuild.py' |
335 | --- lib/lp/buildmaster/tests/test_packagebuild.py 2010-05-13 15:56:00 +0000 |
336 | +++ lib/lp/buildmaster/tests/test_packagebuild.py 2010-05-13 15:56:21 +0000 |
337 | @@ -10,6 +10,7 @@ |
338 | |
339 | from storm.store import Store |
340 | from zope.component import getUtility |
341 | +from zope.security.interfaces import Unauthorized |
342 | from zope.security.proxy import removeSecurityProxy |
343 | |
344 | from canonical.testing.layers import LaunchpadFunctionalLayer |
345 | @@ -20,7 +21,7 @@ |
346 | from lp.buildmaster.model.packagebuild import PackageBuild |
347 | from lp.buildmaster.tests.test_buildbase import TestBuildBaseMixin |
348 | from lp.registry.interfaces.pocket import PackagePublishingPocket |
349 | -from lp.testing import TestCaseWithFactory |
350 | +from lp.testing import login, login_person, TestCaseWithFactory |
351 | |
352 | |
353 | class TestPackageBuildBase(TestCaseWithFactory): |
354 | @@ -121,6 +122,16 @@ |
355 | hashlib.sha1("Some content").hexdigest(), |
356 | self.package_build.upload_log.content.sha1) |
357 | |
358 | + def test_storeUploadLog_private(self): |
359 | + # A private package build will store the upload log on the |
360 | + # restricted librarian. |
361 | + login('admin@canonical.com') |
362 | + self.package_build.archive.buildd_secret = 'sekrit' |
363 | + self.package_build.archive.private = True |
364 | + self.failUnless(self.package_build.is_private) |
365 | + self.package_build.storeUploadLog("Some content") |
366 | + self.failUnless(self.package_build.upload_log.restricted) |
367 | + |
368 | def test_upload_log_url(self): |
369 | # The url of the upload log file is determined by the PackageBuild. |
370 | Store.of(self.package_build).flush() |
371 | @@ -133,6 +144,37 @@ |
372 | build_id, build_id), |
373 | log_url) |
374 | |
375 | + def test_view_package_build(self): |
376 | + # Anonymous access can read public builds, but not edit. |
377 | + self.failUnlessEqual( |
378 | + None, self.package_build.dependencies) |
379 | + self.assertRaises( |
380 | + Unauthorized, setattr, self.package_build, |
381 | + 'dependencies', u'my deps') |
382 | + |
383 | + def test_edit_package_build(self): |
384 | + # An authenticated user who belongs to the owning archive team |
385 | + # can edit the build. |
386 | + login_person(self.package_build.archive.owner) |
387 | + self.package_build.dependencies = u'My deps' |
388 | + self.failUnlessEqual( |
389 | + u'My deps', self.package_build.dependencies) |
390 | + |
391 | + # But other users cannot. |
392 | + other_person = self.factory.makePerson() |
393 | + login_person(other_person) |
394 | + self.assertRaises( |
395 | + Unauthorized, setattr, self.package_build, |
396 | + 'dependencies', u'my deps') |
397 | + |
398 | + |
399 | + def test_admin_package_build(self): |
400 | + # Users with edit access can update attributes. |
401 | + login('admin@canonical.com') |
402 | + self.package_build.dependencies = u'My deps' |
403 | + self.failUnlessEqual( |
404 | + u'My deps', self.package_build.dependencies) |
405 | + |
406 | |
407 | def test_suite(): |
408 | return unittest.TestLoader().loadTestsFromName(__name__) |
409 | |
410 | === renamed file 'lib/lp/soyuz/doc/build.txt' => 'lib/lp/soyuz/doc/binarypackagebuild.txt' |
411 | --- lib/lp/soyuz/doc/build.txt 2010-05-13 15:56:00 +0000 |
412 | +++ lib/lp/soyuz/doc/binarypackagebuild.txt 2010-05-13 15:56:21 +0000 |
413 | @@ -39,10 +39,10 @@ |
414 | >>> print firefox_build.distribution.displayname |
415 | ubuntutest |
416 | |
417 | - >>> print firefox_build.distroseries.displayname |
418 | + >>> print firefox_build.distro_series.displayname |
419 | Breezy Badger Autotest |
420 | |
421 | - >>> print firefox_build.distroarchseries.displayname |
422 | + >>> print firefox_build.distro_arch_series.displayname |
423 | ubuntutest Breezy Badger Autotest i386 |
424 | |
425 | >>> firefox_build.pocket |
426 | @@ -54,7 +54,7 @@ |
427 | >>> firefox_build.is_virtualized |
428 | False |
429 | |
430 | - >>> print firefox_build.sourcepackagerelease.title |
431 | + >>> print firefox_build.source_package_release.title |
432 | mozilla-firefox - 0.9 |
433 | |
434 | A build has an state that represents in which stage it is in a |
435 | @@ -62,12 +62,12 @@ |
436 | of the intermediate failed states (FAILEDTOBUILD, MANUALDEPWAIT, |
437 | CHROOTWAIT, SUPERSEDED and FAILEDTOUPLOAD). |
438 | |
439 | - >>> firefox_build.buildstate |
440 | + >>> firefox_build.status |
441 | <DBItem BuildStatus.FULLYBUILT, (1) Successfully built> |
442 | |
443 | Builds which were already processed also offer additional information |
444 | about its process such as the time it was started and finished and its |
445 | -'buildlog' and 'upload_changesfile' as librarian files. |
446 | +'log' and 'upload_changesfile' as librarian files. |
447 | |
448 | >>> firefox_build.was_built |
449 | True |
450 | @@ -75,16 +75,16 @@ |
451 | >>> firefox_build.date_started |
452 | datetime.datetime(...) |
453 | |
454 | - >>> firefox_build.datebuilt |
455 | + >>> firefox_build.date_finished |
456 | datetime.datetime(...) |
457 | |
458 | - >>> firefox_build.buildduration |
459 | + >>> firefox_build.duration |
460 | datetime.timedelta(...) |
461 | |
462 | - >>> print firefox_build.buildlog.filename |
463 | + >>> print firefox_build.log.filename |
464 | buildlog_ubuntutest-breezy-autotest-i386.mozilla-firefox_0.9_FULLYBUILT.txt.gz |
465 | |
466 | - >>> print firefox_build.build_log_url |
467 | + >>> print firefox_build.log_url |
468 | http://launchpad.dev/ubuntutest/+source/mozilla-firefox/0.9/+build/.../+files/buildlog_ubuntutest-breezy-autotest-i386.mozilla-firefox_0.9_FULLYBUILT.txt.gz |
469 | |
470 | >>> print firefox_build.upload_changesfile.filename |
471 | @@ -119,7 +119,7 @@ |
472 | |
473 | It is not necessarily the same as: |
474 | |
475 | - >>> print firefox_build.sourcepackagerelease.component.name |
476 | + >>> print firefox_build.source_package_release.component.name |
477 | main |
478 | |
479 | which is the component the source was originally uploaded to, before |
480 | @@ -151,7 +151,7 @@ |
481 | >>> frozen_build = getUtility(IBinaryPackageBuildSet).getByBuildID(9) |
482 | >>> frozen_build.title |
483 | u'i386 build of pmount 0.1-1 in ubuntu warty RELEASE' |
484 | - >>> frozen_build.buildstate.title |
485 | + >>> frozen_build.status.title |
486 | 'Failed to build' |
487 | >>> frozen_build.can_be_retried |
488 | False |
489 | @@ -166,13 +166,13 @@ |
490 | >>> print active_build.title |
491 | i386 build of pmount 0.1-1 in ubuntu warty RELEASE |
492 | |
493 | - >>> print active_build.buildstate.name |
494 | + >>> print active_build.status.name |
495 | FAILEDTOBUILD |
496 | |
497 | >>> print active_build.builder.name |
498 | bob |
499 | |
500 | - >>> print active_build.buildlog.filename |
501 | + >>> print active_build.log.filename |
502 | netapplet-1.0.0.tar.gz |
503 | |
504 | At this point, it's also convenient to test if any content can be |
505 | @@ -210,54 +210,7 @@ |
506 | sample upload log. |
507 | |
508 | The 'upload_log' library file privacy is set according to the build |
509 | -target archive. |
510 | - |
511 | - >>> print active_build.archive.private |
512 | - False |
513 | - |
514 | - >>> print active_build.upload_log.restricted |
515 | - False |
516 | - |
517 | -Transforming ubuntu primary archive in a private archive makes |
518 | -storeUploadLog() upload the given content as a restricted file. |
519 | - |
520 | - >>> login('foo.bar@canonical.com') |
521 | - >>> original_archive = active_build.archive |
522 | - >>> private_ppa = factory.makeArchive(private=True) |
523 | - >>> removeSecurityProxy(active_build).archive = private_ppa |
524 | - >>> login(ANONYMOUS) |
525 | - |
526 | -Simply changing the archive privacy after the 'upload_log' is stored |
527 | -doesn't make it restricted. The archive privacy will be respected in |
528 | -the next upload attempt only. |
529 | - |
530 | - >>> print active_build.upload_log.restricted |
531 | - False |
532 | - |
533 | -In order to call storeUploadLog() again for the same build, we have to |
534 | -remove the previously stored 'upload_log'. See the check below in |
535 | -'AssertionErrors in IBinaryPackageBuild' for more detail. |
536 | - |
537 | - >>> login('foo.bar@canonical.com') |
538 | - >>> active_build.upload_log = None |
539 | - >>> login(ANONYMOUS) |
540 | - |
541 | -Once targeted to a private archive the 'upload_log' gets stored in the |
542 | -restricted librarian instance. |
543 | - |
544 | - >>> print active_build.archive.private |
545 | - True |
546 | - |
547 | - >>> active_build.storeUploadLog('private upload log.') |
548 | - >>> print active_build.upload_log.restricted |
549 | - True |
550 | - |
551 | -Restore ubuntu main archive as the target to not affect the rest of the |
552 | -tests. |
553 | - |
554 | - >>> login('foo.bar@canonical.com') |
555 | - >>> removeSecurityProxy(active_build).archive = original_archive |
556 | - >>> login(ANONYMOUS) |
557 | +target archive. For an example, see lp.buildmaster.tests.test_packagebuild |
558 | |
559 | Since ubuntu/warty is already released the failed build can't be |
560 | retried. |
561 | @@ -318,18 +271,18 @@ |
562 | >>> print active_build.builder |
563 | None |
564 | |
565 | - >>> print active_build.buildstate.name |
566 | + >>> print active_build.status.name |
567 | NEEDSBUILD |
568 | |
569 | >>> print active_build.buildqueue_record |
570 | <...BuildQueue...> |
571 | |
572 | -'buildlog' and 'upload_log' librarian references were removed when the |
573 | +'log' and 'upload_log' librarian references were removed when the |
574 | build was retried. They will be garbage-collected later by |
575 | 'librariangc' and replaced by new ones when the build re-attempt |
576 | finishes. |
577 | |
578 | - >>> print active_build.buildlog |
579 | + >>> print active_build.log |
580 | None |
581 | |
582 | >>> print active_build.upload_log |
583 | @@ -353,8 +306,8 @@ |
584 | >>> description = 'Descripppppppption' |
585 | >>> from canonical.launchpad.interfaces import BinaryPackageFormat |
586 | >>> binpackageformat = BinaryPackageFormat.DEB |
587 | - >>> component = firefox_build.sourcepackagerelease.component.id |
588 | - >>> section = firefox_build.sourcepackagerelease.section.id |
589 | + >>> component = firefox_build.source_package_release.component.id |
590 | + >>> section = firefox_build.source_package_release.section.id |
591 | >>> from canonical.launchpad.interfaces import PackagePublishingPriority |
592 | >>> priority = PackagePublishingPriority.STANDARD |
593 | >>> shlibdeps = None |
594 | @@ -565,7 +518,8 @@ |
595 | ... return row[0].id |
596 | >>> for row in sorted(rset, key=sort_result_key): |
597 | ... (sourcepackagerelease, buildlog, |
598 | - ... sourcepackagename, buildlog_content, builder) = row |
599 | + ... sourcepackagename, buildlog_content, builder, |
600 | + ... package_build, build_farm_job) = row |
601 | ... print( |
602 | ... 'builder: %s, spr: %s, log: %s' % |
603 | ... (id_or_none(builder), |
604 | @@ -662,7 +616,7 @@ |
605 | >>> [deleted_build, superseded_build] == list(old_source_builds) |
606 | True |
607 | |
608 | - >>> deleted_build.datecreated > superseded_build.datecreated |
609 | + >>> deleted_build.date_created > superseded_build.date_created |
610 | True |
611 | |
612 | Builds records for the exactly the same `SourcePackageRelease`s may |
613 | @@ -699,9 +653,9 @@ |
614 | >>> ubuntu.getBuildRecords().count() |
615 | 17 |
616 | |
617 | - >>> L = ubuntu.getBuildRecords(build_state=BuildStatus.FULLYBUILT) |
618 | - >>> for l in L: |
619 | - ... print l.datebuilt, l.id, l.buildstate.value |
620 | + >>> builds = ubuntu.getBuildRecords(build_state=BuildStatus.FULLYBUILT) |
621 | + >>> for build in builds: |
622 | + ... print build.date_finished, build.id, build.status.value |
623 | 2007-08-10 00:00:14+00:00 30 1 |
624 | 2007-08-09 23:59:59+00:00 29 1 |
625 | 2005-03-25 00:00:03+00:00 7 1 |
626 | @@ -727,7 +681,7 @@ |
627 | Note that they are ordered by DESC lastscore, as expected: |
628 | |
629 | >>> for b in builds: |
630 | - ... b.id, b.buildstate.value, b.buildqueue_record.lastscore |
631 | + ... b.id, b.status.value, b.buildqueue_record.lastscore |
632 | (11, 0, 10) |
633 | (9, 0, 0) |
634 | |
635 | @@ -906,14 +860,6 @@ |
636 | >>> gina_build.was_built |
637 | True |
638 | |
639 | -this method is protected by an assertion on valid buildduration and |
640 | -datebuilt, it makes the diagnosis of problems in current DB easier. |
641 | - |
642 | - >>> gina_build.date_started |
643 | - Traceback (most recent call last): |
644 | - ... |
645 | - AssertionError: value is not suitable for this build record (10) |
646 | - |
647 | Only builds in failed_states (FAILEDTOBUILD, MANUALDEPWAIT and |
648 | CHROOTWAIT) can be retried. We must check if Soyuz is able to accept |
649 | its result in case of success, i.e., we should not be able to retry a |
650 | @@ -927,7 +873,7 @@ |
651 | >>> failed_build.title |
652 | u'i386 build of foobar 1.0 in ubuntu warty RELEASE' |
653 | |
654 | - >>> failed_build.buildstate.name |
655 | + >>> failed_build.status.name |
656 | 'FAILEDTOBUILD' |
657 | |
658 | >>> failed_build.can_be_retried |
659 | @@ -1024,12 +970,12 @@ |
660 | See also bug 177827. |
661 | |
662 | >>> login('foo.bar@canonical.com') |
663 | - >>> depwait_build.dependencies = 'pmount' |
664 | + >>> depwait_build.dependencies = u'pmount' |
665 | >>> flush_database_updates() |
666 | |
667 | 'pmount' in hoary/i386 is published in the 'universe' component: |
668 | |
669 | - >>> hoary_i386 = depwait_build.distroarchseries |
670 | + >>> hoary_i386 = depwait_build.distro_arch_series |
671 | >>> pmount_pub = hoary_i386[ |
672 | ... 'pmount'].currentrelease.current_publishing_record |
673 | >>> print pmount_pub.component.name |
674 | @@ -1059,7 +1005,7 @@ |
675 | >>> from lp.soyuz.interfaces.component import IComponentSet |
676 | >>> main_component = getUtility(IComponentSet)['main'] |
677 | >>> pmount_pub.component = main_component |
678 | - >>> depwait_build.dependencies = 'mozilla-firefox, pmount' |
679 | + >>> depwait_build.dependencies = u'mozilla-firefox, pmount' |
680 | >>> from canonical.database.sqlbase import flush_database_caches |
681 | >>> flush_database_caches() |
682 | >>> login(ANONYMOUS) |
683 | @@ -1080,7 +1026,7 @@ |
684 | |
685 | |
686 | >>> login('foo.bar@canonical.com') |
687 | - >>> depwait_build.dependencies = 'biscuit, pmount' |
688 | + >>> depwait_build.dependencies = u'biscuit, pmount' |
689 | >>> universe_component = getUtility(IComponentSet)['universe'] |
690 | >>> pmount_pub.component = universe_component |
691 | >>> removeSecurityProxy(depwait_build).archive = cprov.archive |
692 | @@ -1112,16 +1058,16 @@ |
693 | for performing the mentioned auto-depwait procedure we have a utility |
694 | in IBinaryPackageBuildSet called retryDepWaiting(). |
695 | |
696 | - >>> print depwait_build.buildstate.name |
697 | + >>> print depwait_build.status.name |
698 | MANUALDEPWAIT |
699 | - >>> print depwait_build.distroarchseries.title |
700 | + >>> print depwait_build.distro_arch_series.title |
701 | The Hoary Hedgehog Release for i386 (x86) |
702 | |
703 | In order to allow depwait_build to be retried we will forge a |
704 | 'fully-satisfiable' dependencies field. |
705 | |
706 | >>> login('foo.bar@canonical.com') |
707 | - >>> depwait_build.dependencies = 'pmount' |
708 | + >>> depwait_build.dependencies = u'pmount' |
709 | >>> login(ANONYMOUS) |
710 | >>> flush_database_updates() |
711 | |
712 | @@ -1130,7 +1076,7 @@ |
713 | |
714 | >>> getUtility(IBinaryPackageBuildSet).retryDepWaiting(hoaryi386) |
715 | |
716 | - >>> print depwait_build.buildstate.name |
717 | + >>> print depwait_build.status.name |
718 | NEEDSBUILD |
719 | |
720 | >>> depwait_build.buildqueue_record.lastscore |
721 | @@ -1165,7 +1111,7 @@ |
722 | If a callsite tries to rescore a build that is not in the NEEDSBUILD state, |
723 | a CannotBeRescored exception is raised. |
724 | |
725 | - >>> depwait_build.buildstate = BuildStatus.FAILEDTOUPLOAD |
726 | + >>> depwait_build.status = BuildStatus.FAILEDTOUPLOAD |
727 | >>> depwait_build.rescore(1000) |
728 | Traceback (most recent call last): |
729 | ... |
730 | @@ -1246,12 +1192,12 @@ |
731 | ... print "Did not fail when expected setting %s" % property |
732 | >>> from canonical.database.constants import UTC_NOW |
733 | >>> properties = { |
734 | - ... 'buildlog': 1, |
735 | - ... 'datebuilt': UTC_NOW, |
736 | - ... 'buildduration': None, |
737 | + ... 'log': 1, |
738 | + ... 'date_finished': UTC_NOW, |
739 | + ... 'date_started': None, |
740 | ... 'builder': bob, |
741 | - ... 'buildstate': BuildStatus.FAILEDTOUPLOAD, |
742 | - ... 'dependencies': 'whatever', |
743 | + ... 'status': BuildStatus.FAILEDTOUPLOAD, |
744 | + ... 'dependencies': u'whatever', |
745 | ... 'upload_log': 1, |
746 | ... } |
747 | |
748 | @@ -1295,8 +1241,8 @@ |
749 | ... PackagePublishingPocket.UPDATES, |
750 | ... test_publisher.breezy_autotest.main_archive, |
751 | ... status=BuildStatus.FULLYBUILT) |
752 | - >>> oldest_build.buildduration = timedelta(minutes=72) |
753 | - >>> oldest_build.datebuilt = datetime(2008, 4, 1, 10, 45, 39, tzinfo=UTC) |
754 | + >>> oldest_build.date_finished = datetime(2008, 4, 1, 10, 45, 39, tzinfo=UTC) |
755 | + >>> oldest_build.date_started = oldest_build.date_finished - timedelta(minutes=72) |
756 | |
757 | Check that the oldest build instance's 'estimated_duration' |
758 | is initialized based on its package size. Since the latter is very |
759 | @@ -1320,8 +1266,8 @@ |
760 | ... PackagePublishingPocket.UPDATES, |
761 | ... test_publisher.breezy_autotest.main_archive, |
762 | ... status=BuildStatus.FULLYBUILT) |
763 | - >>> medium_build.buildduration = timedelta(minutes=60) |
764 | - >>> medium_build.datebuilt = datetime(2008, 4, 2, 11, 56, 33, tzinfo=UTC) |
765 | + >>> medium_build.date_finished = datetime(2008, 4, 2, 11, 56, 33, tzinfo=UTC) |
766 | + >>> medium_build.date_started = medium_build.date_finished - timedelta(minutes=60) |
767 | |
768 | Check whether the intermediate build instance's 'estimated_duration' |
769 | value equals the oldest instance's 'buildduration' (72 minutes equals 4320 |
770 | @@ -1408,7 +1354,7 @@ |
771 | ... source_ids) |
772 | >>> import operator |
773 | >>> for build in sorted(builds, key=operator.attrgetter("id")): |
774 | - ... print build.title, build.buildstate.name |
775 | + ... print build.title, build.status.name |
776 | hppa build of sourceone 666 in ubuntutest breezy-autotest RELEASE |
777 | FULLYBUILT |
778 | hppa build of sourcetwo 666 in ubuntutest breezy-autotest RELEASE |
779 | @@ -1419,7 +1365,7 @@ |
780 | >>> builds = removeSecurityProxy(bs).getBuildsBySourcePackageRelease( |
781 | ... source_ids, buildstate=BuildStatus.FULLYBUILT) |
782 | >>> for build in sorted(builds, key=operator.attrgetter("id")): |
783 | - ... print build.title, build.buildstate.name |
784 | + ... print build.title, build.status.name |
785 | hppa build of sourceone 666 in ubuntutest breezy-autotest RELEASE |
786 | FULLYBUILT |
787 | |
788 | @@ -1463,13 +1409,13 @@ |
789 | >>> bob_builder = getUtility(IBuilderSet)['bob'] |
790 | |
791 | >>> earlier_builds = src_pkg_earlier.createMissingBuilds() |
792 | - >>> eg_build_date = earlier_builds[0].datecreated |
793 | + >>> eg_build_date = earlier_builds[0].date_created |
794 | >>> for build in earlier_builds: |
795 | - ... build.datebuilt = eg_build_date - timedelta(1) |
796 | + ... build.date_finished = eg_build_date - timedelta(1) |
797 | |
798 | >>> later_builds = src_pkg_later.createMissingBuilds() |
799 | >>> for build in later_builds: |
800 | - ... build.datebuilt = eg_build_date |
801 | + ... build.date_finished = eg_build_date |
802 | |
803 | Ensure that the i386 builds are created by the 'frog' builder, |
804 | while the hppa builds are created by 'bob' the builder: |
805 | |
806 | === modified file 'lib/lp/soyuz/doc/buildd-mass-retry.txt' |
807 | --- lib/lp/soyuz/doc/buildd-mass-retry.txt 2010-04-21 11:13:11 +0000 |
808 | +++ lib/lp/soyuz/doc/buildd-mass-retry.txt 2010-05-13 15:56:21 +0000 |
809 | @@ -25,7 +25,7 @@ |
810 | |
811 | The script provides dry-run mode (-N). |
812 | |
813 | -See build.txt for more information about IBuild.retry(). |
814 | +See binarypackagebuild.txt for more information about IBuild.retry(). |
815 | |
816 | Passing only suite, request retry on all failed states: |
817 | |
818 | |
819 | === modified file 'lib/lp/soyuz/model/binarypackagebuild.py' |
820 | --- lib/lp/soyuz/model/binarypackagebuild.py 2010-05-13 15:56:00 +0000 |
821 | +++ lib/lp/soyuz/model/binarypackagebuild.py 2010-05-13 15:56:21 +0000 |
822 | @@ -12,6 +12,8 @@ |
823 | import operator |
824 | |
825 | from storm.locals import Int, Reference |
826 | +from canonical.launchpad.webapp.interfaces import ( |
827 | + IStoreSelector, MAIN_STORE, DEFAULT_FLAVOR) |
828 | |
829 | from zope.interface import implements |
830 | from zope.component import getUtility |
831 | @@ -185,11 +187,6 @@ |
832 | return self.archive.require_virtualized |
833 | |
834 | @property |
835 | - def is_private(self): |
836 | - """See `IBuildBase`""" |
837 | - return self.archive.private |
838 | - |
839 | - @property |
840 | def title(self): |
841 | """See `IBuild`""" |
842 | return '%s build of %s %s in %s %s %s' % ( |
843 | @@ -212,7 +209,7 @@ |
844 | |
845 | @property |
846 | def log_url(self): |
847 | - """See `IBuildFarmJob`. |
848 | + """See `IPackageBuild`. |
849 | |
850 | Overridden here for the case of builds for distro archives, |
851 | currently only supported for binary package builds. |
852 | @@ -222,6 +219,17 @@ |
853 | return ProxiedLibraryFileAlias(self.log, self).http_url |
854 | |
855 | @property |
856 | + def upload_log_url(self): |
857 | + """See `IPackageBuild`. |
858 | + |
859 | + Overridden here for the case of builds for distro archives, |
860 | + currently only supported for binary package builds. |
861 | + """ |
862 | + if self.upload_log is None: |
863 | + return None |
864 | + return ProxiedLibraryFileAlias(self.upload_log, self).http_url |
865 | + |
866 | + @property |
867 | def distributionsourcepackagerelease(self): |
868 | """See `IBuild`.""" |
869 | from lp.soyuz.model.distributionsourcepackagerelease \ |
870 | @@ -284,10 +292,10 @@ |
871 | """See `IBuild`.""" |
872 | assert self.can_be_retried, "Build %s cannot be retried" % self.id |
873 | self.status = BuildStatus.NEEDSBUILD |
874 | - self.datebuilt = None |
875 | - self.buildduration = None |
876 | + self.date_finished = None |
877 | + self.date_started = None |
878 | self.builder = None |
879 | - self.buildlog = None |
880 | + self.log = None |
881 | self.upload_log = None |
882 | self.dependencies = None |
883 | self.queueBuild() |
884 | @@ -700,7 +708,7 @@ |
885 | package_build = getUtility(IPackageBuildSource).new( |
886 | BinaryPackageBuild.build_farm_job_type, |
887 | archive.require_virtualized, archive, pocket, processor, |
888 | - status) #, date_created) |
889 | + status, date_created=date_created) |
890 | |
891 | binary_package_build = BinaryPackageBuild( |
892 | package_build=package_build, |
893 | @@ -711,8 +719,9 @@ |
894 | def getBuildBySRAndArchtag(self, sourcepackagereleaseID, archtag): |
895 | """See `IBinaryPackageBuildSet`""" |
896 | clauseTables = ['DistroArchSeries'] |
897 | - query = ('Build.sourcepackagerelease = %s ' |
898 | - 'AND Build.distroarchseries = DistroArchSeries.id ' |
899 | + query = ('BinaryPackageBuild.source_package_release = %s ' |
900 | + 'AND BinaryPackageBuild.distro_arch_series = ' |
901 | + 'DistroArchSeries.id ' |
902 | 'AND DistroArchSeries.architecturetag = %s' |
903 | % sqlvalues(sourcepackagereleaseID, archtag) |
904 | ) |
905 | @@ -896,7 +905,7 @@ |
906 | # * FULLYBUILT & FAILURES by -datebuilt |
907 | # It should present the builds in a more natural order. |
908 | if status in [BuildStatus.NEEDSBUILD, BuildStatus.BUILDING]: |
909 | - orderBy = ["-BuildQueue.lastscore", "Build.id"] |
910 | + orderBy = ["-BuildQueue.lastscore", "BinaryPackageBuild.id"] |
911 | clauseTables.append('BuildQueue') |
912 | clauseTables.append('BuildPackageJob') |
913 | condition_clauses.append( |
914 | @@ -939,9 +948,14 @@ |
915 | logger = logging.getLogger('retry-depwait') |
916 | |
917 | # Get the MANUALDEPWAIT records for all archives. |
918 | - candidates = BinaryPackageBuild.selectBy( |
919 | - status=BuildStatus.MANUALDEPWAIT, |
920 | - distro_arch_series=distroarchseries) |
921 | + store = getUtility(IStoreSelector).get(MAIN_STORE, DEFAULT_FLAVOR) |
922 | + |
923 | + candidates = store.find( |
924 | + BinaryPackageBuild, |
925 | + BinaryPackageBuild.distro_arch_series == distroarchseries, |
926 | + BinaryPackageBuild.package_build == PackageBuild.id, |
927 | + PackageBuild.build_farm_job == BuildFarmJob.id, |
928 | + BuildFarmJob.status == BuildStatus.MANUALDEPWAIT) |
929 | |
930 | candidates_count = candidates.count() |
931 | if candidates_count == 0: |
932 | @@ -971,17 +985,19 @@ |
933 | return [] |
934 | |
935 | query = """ |
936 | - sourcepackagerelease IN %s AND |
937 | - archive.id = build.archive AND |
938 | - archive.purpose != %s |
939 | + source_package_release IN %s AND |
940 | + package_build = packagebuild.id AND |
941 | + archive.id = packagebuild.archive AND |
942 | + archive.purpose != %s AND |
943 | + packagebuild.build_farm_job = buildfarmjob.id |
944 | """ % sqlvalues(sourcepackagerelease_ids, ArchivePurpose.PPA) |
945 | |
946 | if buildstate is not None: |
947 | - query += "AND buildstate = %s" % sqlvalues(buildstate) |
948 | + query += "AND buildfarmjob.status = %s" % sqlvalues(buildstate) |
949 | |
950 | return BinaryPackageBuild.select( |
951 | - query, orderBy=["-datecreated", "id"], |
952 | - clauseTables=["Archive"]) |
953 | + query, orderBy=["-buildfarmjob.date_created", "id"], |
954 | + clauseTables=["Archive", "PackageBuild", "BuildFarmJob"]) |
955 | |
956 | def getStatusSummaryForBuilds(self, builds): |
957 | """See `IBinaryPackageBuildSet`.""" |
958 | |
959 | === modified file 'lib/lp/soyuz/stories/webservice/xx-builds.txt' |
960 | --- lib/lp/soyuz/stories/webservice/xx-builds.txt 2010-04-09 15:46:09 +0000 |
961 | +++ lib/lp/soyuz/stories/webservice/xx-builds.txt 2010-05-13 15:56:21 +0000 |
962 | @@ -92,7 +92,7 @@ |
963 | |
964 | If a build is in a retry-able state, the retry method can be invoked |
965 | to cause a new build request for that build. The caller must also have |
966 | -permission to retry the build. See doc/build.txt and |
967 | +permission to retry the build. See doc/binarypackagebuild.txt and |
968 | stories/soyuz/xx-build-record.txt for more information. |
969 | |
970 | >>> a_build = builds['entries'][0] |