Merge lp:~doanac/ubuntu-ci-services-itself/bsbuilder-better-error-handling into lp:ubuntu-ci-services-itself

Proposed by Andy Doan
Status: Merged
Merged at revision: 355
Proposed branch: lp:~doanac/ubuntu-ci-services-itself/bsbuilder-better-error-handling
Merge into: lp:ubuntu-ci-services-itself
Prerequisite: lp:~doanac/ubuntu-ci-services-itself/bsbuilder-pass-subticket
Diff against target: 470 lines (+273/-40)
5 files modified
branch-source-builder/bsbuilder/run_worker.py (+103/-23)
branch-source-builder/cupstream2distro/packageinppamanager.py (+4/-7)
branch-source-builder/setup.py (+2/-1)
branch-source-builder/watch_ppa.py (+163/-8)
juju-deployer/branch-source-builder.yaml.tmpl (+1/-1)
To merge this branch: bzr merge lp:~doanac/ubuntu-ci-services-itself/bsbuilder-better-error-handling
Reviewer Review Type Date Requested Status
Francis Ginther Approve
PS Jenkins bot (community) continuous-integration Approve
Review via email: mp+210312@code.launchpad.net

Commit message

bsbuilder: fix serval error cases

This branch helps prevent:

* returning success when an upload fails
* ensuring a package version is bumped if a prior version of the
  package has landed in the ppa before
* the package wasn't signed

Description of the change

This is another piece of the fginther bsb-fixes branch. This helps give better errors information back to the user.

I'd like to get tests written for the upload_ppa logic some day, but I think that's aiming a little too high for this week.

I've tested this with two tickets: 1 showed a failure because there was a newer package version in the PPA. The other was a proper package that built.

To post a comment you must log in.
Revision history for this message
PS Jenkins bot (ps-jenkins) wrote :

PASSED: Continuous integration, rev:355
http://s-jenkins.ubuntu-ci:8080/job/uci-engine-ci/367/
Executed test runs:

Click here to trigger a rebuild:
http://s-jenkins.ubuntu-ci:8080/job/uci-engine-ci/367/rebuild

review: Approve (continuous-integration)
Revision history for this message
Francis Ginther (fginther) wrote :

This is a good separation of the package building (check and watch) updates. As Andy was able to refactor this, I'll approve.

review: Approve

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'branch-source-builder/bsbuilder/run_worker.py'
2--- branch-source-builder/bsbuilder/run_worker.py 2014-03-10 22:57:01 +0000
3+++ branch-source-builder/bsbuilder/run_worker.py 2014-03-10 22:57:01 +0000
4@@ -16,13 +16,16 @@
5
6 import json
7 import logging
8+import os
9 import time
10+import yaml
11
12-from bsbuilder import upload_package
13+import upload_package
14 import watch_ppa
15
16 from ci_utils import (
17 amqp_utils,
18+ data_store,
19 dump_stack,
20 )
21
22@@ -32,19 +35,64 @@
23 TIME_BETWEEN_CHECKS = 60
24
25
26+def get_auth_config():
27+ DEFAULT_CONFIG_YAML = os.path.join(
28+ os.path.dirname(__file__), '../../unit_config')
29+ with open(DEFAULT_CONFIG_YAML) as f:
30+ config = yaml.safe_load(f)
31+ return config
32+
33+
34+def create_result_file(datastore, content):
35+ '''Creates a result file based on the final upload and build status.
36+
37+ The input content has the following format:
38+ {
39+ 'message': 'The high level status of the upload',
40+ 'subticket_status': [{
41+ 'name': source_package_name,
42+ 'version': source_package_version,
43+ 'step': numeric_workflow_step,
44+ 'step_text': text_workflow_step,
45+ 'status': numeric_workflow_status,
46+ 'status_text': text_workflow_status,
47+ 'message': subticket_specific_message,
48+ }]
49+ }
50+
51+ The return is an artifact entry for the progress message.'''
52+ package_build_result_filename = 'package_build.output.log'
53+ body = '{}\n\n'.format(content['message'])
54+ body += 'Uploaded source packages:\n'
55+ for subticket_status in content['subticket_status']:
56+ body += 'name: {}\n'.format(subticket_status['name'])
57+ body += 'version: {}\n'.format(subticket_status['version'])
58+ body += 'status: {}\n'.format(subticket_status['status_text'])
59+ if subticket_status.get('message'):
60+ body += 'message: {}\n'.format(subticket_status['message'])
61+ location = datastore.put_file(
62+ package_build_result_filename, str(body), 'text/plain')
63+ return {
64+ 'name': package_build_result_filename,
65+ 'type': 'LOGS',
66+ 'reference': location,
67+ }
68+
69+
70 def on_message(msg):
71 log.info('on_message: {}'.format(msg.body))
72 params = json.loads(msg.body)
73+ ticket_id = params['ticket_id']
74 subtickets = params['subtickets']
75 series = params['series']
76 ppa = params['ppa']
77 archive = params['archive']
78- log.info('The PPA is: {}'.format(ppa))
79 trigger = params['progress_trigger']
80
81 # Setup the output data to send back to the caller
82 # TODO: artifacts will be populated with artifacts from this build.
83 out_data = {
84+ 'message': 'Build in progress.',
85 'ppa': ppa,
86 'archive': archive,
87 'artifacts': [],
88@@ -52,31 +100,63 @@
89 amqp_utils.progress_update(trigger, params)
90
91 try:
92+ datastore = data_store.create_for_ticket(ticket_id, get_auth_config())
93+
94 upload_list = upload_package.upload_source_packages(ppa, subtickets)
95 log.info('upload_list: {}'.format(upload_list))
96- start_time = time.time()
97- while True:
98- (ret, status) = watch_ppa.watch_ppa(start_time, series, ppa,
99- archive, None,
100- upload_list)
101- progress = {}
102- for key in status:
103- progress[key] = str(status[key])
104- log.info('progress: {}'.format(progress))
105- out_data['status'] = progress
106- amqp_utils.progress_update(trigger, out_data)
107- if ret == -1:
108- log.info('Going to sleep for {}'.format(TIME_BETWEEN_CHECKS))
109- time.sleep(TIME_BETWEEN_CHECKS)
110+
111+ # Check that an upload isn't going to fail due to version
112+ (failed, subticket_status) = watch_ppa.check_ppa(
113+ series, ppa, archive, None, upload_list)
114+ if failed:
115+ # This is the end of the line, the upload is blocked.
116+ out_data['subticket_status'] = subticket_status
117+ message = 'Source package upload failed.'
118+ log.error(message)
119+ out_data['message'] = message
120+ out_data['artifacts'].append(
121+ create_result_file(datastore, out_data))
122+ amqp_utils.progress_failed(trigger, out_data)
123+ else:
124+ # Set ret to 1, the upload is failed until it succeeds
125+ ret = 1
126+ start_time = time.time()
127+ while True:
128+ (ret, subticket_status) = watch_ppa.watch_ppa(
129+ start_time, series, ppa, archive, None, upload_list)
130+ log.info('subticket_status: {}'.format(subticket_status))
131+ out_data['subticket_status'] = subticket_status
132+ if ret == -1:
133+ # -1 indicates that the task is not finished
134+ # Send regular progress back to the lander
135+ amqp_utils.progress_update(trigger, out_data)
136+ log.info('Going to sleep for '
137+ '{}'.format(TIME_BETWEEN_CHECKS))
138+ time.sleep(TIME_BETWEEN_CHECKS)
139+ else:
140+ log.info('All done')
141+ break
142+
143+ if ret == 1:
144+ # '1' indicates a failure occurred during the upload/build
145+ message = 'A package failed to upload or build.'
146+ log.error(message)
147+ out_data['message'] = message
148+ out_data['artifacts'].append(
149+ create_result_file(datastore, out_data))
150+ amqp_utils.progress_failed(trigger, out_data)
151 else:
152- log.info('All done')
153- break
154-
155- amqp_utils.progress_completed(trigger, out_data)
156+ # '0' indicates the build was successful
157+ message = 'The package build was successful.'
158+ log.error(message)
159+ out_data['message'] = message
160+ out_data['artifacts'].append(
161+ create_result_file(datastore, out_data))
162+ amqp_utils.progress_completed(trigger, out_data)
163 except (KeyboardInterrupt, Exception) as e:
164- error_msg = 'Exception: {}'.format(e)
165- log.error(error_msg)
166- out_data['error_message'] = error_msg
167+ message = 'Exception: {}'.format(e)
168+ log.error(message)
169+ out_data['message'] = message
170 amqp_utils.progress_failed(trigger, out_data)
171 if isinstance(e, KeyboardInterrupt):
172 raise # re-raise so amqp_utils.process_queue can exit
173
174=== modified file 'branch-source-builder/cupstream2distro/packageinppamanager.py'
175--- branch-source-builder/cupstream2distro/packageinppamanager.py 2014-02-19 16:34:46 +0000
176+++ branch-source-builder/cupstream2distro/packageinppamanager.py 2014-03-10 22:57:01 +0000
177@@ -49,12 +49,6 @@
178 rev = _get_current_rev_from_config(substract[0])
179 branch = _get_parent_branch(substract[0])
180 result.add((substract[0], version, rev, branch))
181- foo = set()
182- foo.add(('autopilot', '1.3.1+13.10.20131003.1-0ubuntu2~fginther.1',
183- '100', 'lp:autopilot'))
184- foo.add(('autopilot', '1.3.1+13.10.20131003.1-0ubuntu2~fginther.2',
185- '105', 'lp:autopilot'))
186- return foo
187 return result
188
189
190@@ -74,7 +68,9 @@
191 return result
192
193
194-def update_all_packages_status(packages_not_in_ppa, packages_building, packages_failed, particular_arch=None):
195+def update_all_packages_status(packages_not_in_ppa, packages_building,
196+ packages_failed, packages_complete,
197+ particular_arch=None):
198 '''Update all packages status, checking in the ppa'''
199
200 for current_package in (packages_not_in_ppa.union(packages_building)):
201@@ -93,6 +89,7 @@
202 elif package_status == PackageInPPA.PUBLISHED:
203 _ensure_removed_from_set(packages_building, current_package) # in case we missed the "build" step
204 _ensure_removed_from_set(packages_not_in_ppa, current_package) # in case we missed the "wait" step
205+ packages_complete.add(current_package)
206
207
208 def _get_current_packaging_version_from_config(source_package_name):
209
210=== modified file 'branch-source-builder/setup.py'
211--- branch-source-builder/setup.py 2014-03-10 22:57:01 +0000
212+++ branch-source-builder/setup.py 2014-03-10 22:57:01 +0000
213@@ -24,10 +24,11 @@
214
215 requires = [
216 'WebTest==2.0.10',
217+ 'amqplib==1.0.0',
218 'chardet>=2.0.1',
219 'dput>=1.6',
220- 'amqplib==1.0.0',
221 'launchpadlib==1.10.2',
222+ 'lazr.enum>=1.1.2',
223 'mock==1.0.1',
224 'restish==0.12.1',
225 ]
226
227=== modified file 'branch-source-builder/watch_ppa.py'
228--- branch-source-builder/watch_ppa.py 2014-02-28 09:25:47 +0000
229+++ branch-source-builder/watch_ppa.py 2014-03-10 22:57:01 +0000
230@@ -22,7 +22,9 @@
231 import json
232 import logging
233 import os
234+import subprocess
235 import sys
236+import textwrap
237 import time
238
239 from cupstream2distro import (launchpadmanager, packageinppamanager)
240@@ -33,6 +35,27 @@
241
242 sys.path.append(os.path.join(os.path.dirname(__file__), '../ci-utils'))
243 from ci_utils import dump_stack
244+from ci_utils.ticket_states import (
245+ SubTicketWorkflowStep,
246+ SubTicketWorkflowStepStatus
247+)
248+
249+EXISTING_SOURCE_MESSAGE = textwrap.dedent('''\
250+ ERROR: A source package with an equal or higher version
251+ of {} exists in {}.
252+ The upload of {} with version {} is failed.
253+ Source package uploads must have a higher version.''')
254+FAILED_SOURCE_MESSAGE = textwrap.dedent('''\
255+ The source package build has failed in {}.
256+ Please review the build logs for the failed package(s).''')
257+MISSING_SOURCE_MESSAGE = textwrap.dedent('''\
258+ The source package for {} with version {}
259+ failed to upload to {}.
260+ Please check that the source package is signed with a signature of
261+ a launchpad user with permission to upload to the PPA.
262+ Also check for a rejection message sent to the package signer.''')
263+PASSED_SOURCE_MESSAGE = textwrap.dedent('''\
264+ The source packages have been built in {}.''')
265
266
267 def parse_arguments():
268@@ -72,6 +95,117 @@
269 return launchpadmanager.get_ppa(ppa)
270
271
272+def create_package_status(package, step, status, message=None):
273+ return {
274+ 'name': package.source_name,
275+ 'version': package.version,
276+ 'step': step.value,
277+ 'step_text': step.title,
278+ 'status': status.value,
279+ 'status_text': status.title,
280+ 'message': message,
281+ }
282+
283+
284+def collect_subticket_status(ppa, packages_not_in_ppa, packages_building,
285+ packages_failed, packages_complete):
286+ '''Collects the sets of package build status into a subticket_status.'''
287+ subticket_status = []
288+ for package in packages_not_in_ppa:
289+ subticket_status.append(create_package_status(
290+ package, SubTicketWorkflowStep.QUEUED,
291+ SubTicketWorkflowStepStatus.PKG_BUILDING_WAITING))
292+
293+ for package in packages_building:
294+ subticket_status.append(create_package_status(
295+ package, SubTicketWorkflowStep.PKG_BUILDING,
296+ SubTicketWorkflowStepStatus.PKG_BUILDING_INPROGRESS))
297+
298+ for package in packages_failed:
299+ subticket_status.append(create_package_status(
300+ package, SubTicketWorkflowStep.COMPLETED,
301+ SubTicketWorkflowStepStatus.PKG_BUILDING_FAILED,
302+ FAILED_SOURCE_MESSAGE.format(ppa.web_link)))
303+
304+ for package in packages_complete:
305+ subticket_status.append(create_package_status(
306+ package, SubTicketWorkflowStep.COMPLETED,
307+ SubTicketWorkflowStepStatus.PKG_BUILDING_COMPLETED,
308+ PASSED_SOURCE_MESSAGE.format(ppa.web_link)))
309+
310+ return subticket_status
311+
312+
313+def get_versions_for_source_package(series, ppa, source_name):
314+ try:
315+ source = ppa.getPublishedSources(exact_match=True,
316+ source_name=source_name,
317+ distro_series=series)[0]
318+ return source.source_package_version
319+ except (KeyError, IndexError):
320+ return None
321+
322+
323+def check_ppa(series, ppa, dest_ppa, arch, upload_list):
324+ # Prepare launchpad connection:
325+ lp_series = launchpadmanager.get_series(series)
326+ monitored_ppa = launchpadmanager.get_ppa(ppa)
327+ if dest_ppa:
328+ dest_archive = get_ppa(dest_ppa)
329+ else:
330+ dest_archive = launchpadmanager.get_ubuntu_archive()
331+ logging.info('Series: {}'.format(lp_series))
332+ logging.info('Monitoring PPA: {}'.format(monitored_ppa))
333+ logging.info('Destination Archive: {}'.format(dest_archive))
334+
335+ failed = False
336+ check_status = []
337+ for source_package in upload_list:
338+ source = source_package['name']
339+ version = source_package['version']
340+ logging.info('Inspecting upload: {} - {}'.format(source, version))
341+ message_list = []
342+ subticket_status = {
343+ 'name': source,
344+ 'id': source_package['id'],
345+ 'version': version,
346+ }
347+ wf_step = SubTicketWorkflowStep.NEW
348+ wf_status = SubTicketWorkflowStepStatus.NEW
349+ check_status.append(subticket_status)
350+ for ppa in [monitored_ppa, dest_archive]:
351+ last_version = get_versions_for_source_package(lp_series, ppa,
352+ source)
353+ logging.info('Last source: {}'.format(last_version))
354+
355+ if last_version:
356+ try:
357+ subprocess.check_call(['dpkg', '--compare-versions',
358+ last_version, 'lt', version])
359+ except subprocess.CalledProcessError:
360+ # The version in the PPA is equal or higher then the
361+ # source package, the build will fail
362+ wf_step = SubTicketWorkflowStep.COMPLETED
363+ wf_status = SubTicketWorkflowStepStatus.PKG_BUILDING_FAILED
364+ message_list.append(EXISTING_SOURCE_MESSAGE.format(
365+ last_version, ppa.web_link, source, version))
366+ failed = True
367+ subticket_status['step'] = wf_step.value
368+ subticket_status['step_text'] = wf_step.title
369+ subticket_status['status'] = wf_status.value
370+ subticket_status['status_text'] = wf_status.title
371+ subticket_status['message'] = '\n'.join(message_list)
372+
373+ return (failed, check_status)
374+
375+
376+def find_subticket(upload_list, name, version):
377+ for subticket in upload_list:
378+ if subticket['name'] == name and subticket['version'] == version:
379+ return subticket
380+ return None
381+
382+
383 def watch_ppa(time_start, series, ppa, dest_ppa, arch, upload_list):
384 # Prepare launchpad connection:
385 lp_series = launchpadmanager.get_series(series)
386@@ -82,7 +216,6 @@
387 dest_archive = launchpadmanager.get_ubuntu_archive()
388 logging.info('Series: {}'.format(lp_series))
389 logging.info('Monitoring PPA: {}'.format(monitored_ppa))
390-
391 logging.info('Destination Archive: {}'.format(dest_archive))
392
393 # Get archs available and archs to ignore
394@@ -102,6 +235,7 @@
395 packages_not_in_ppa = set()
396 packages_building = set()
397 packages_failed = set()
398+ packages_complete = set()
399 for source_package in upload_list:
400 source = source_package['name']
401 version = source_package['version']
402@@ -119,8 +253,6 @@
403 # to eventually appear in the ppa.
404 logging.info('Packages not in PPA: {}'.format(
405 list_packages_info_in_str(packages_not_in_ppa)))
406- logging.info('Packages building: {}'.format(packages_building))
407- logging.info('Packages failed: {}'.format(packages_failed))
408
409 # Check the status regularly on all packages
410 # TODO The following is the original check loop. This can be extracted
411@@ -129,11 +261,24 @@
412 list_packages_info_in_str(
413 packages_not_in_ppa.union(packages_building))))
414 packageinppamanager.update_all_packages_status(
415- packages_not_in_ppa, packages_building, packages_failed, arch)
416-
417- status = {'pending': packages_not_in_ppa,
418- 'building': packages_building,
419- 'failed': packages_failed}
420+ packages_not_in_ppa, packages_building, packages_failed,
421+ packages_complete, arch)
422+
423+ status = collect_subticket_status(monitored_ppa, packages_not_in_ppa,
424+ packages_building, packages_failed,
425+ packages_complete)
426+ for package in status:
427+ subticket = find_subticket(upload_list, package['name'],
428+ package['version'])
429+ if not subticket:
430+ # No match, this should not happen
431+ return (1, status)
432+
433+ # Populate the remaining fields necessary to update ticket status
434+ package['id'] = subticket['id']
435+ package['resource'] = subticket['resource']
436+
437+ logging.info("Status: {}".format(status))
438 # if we have no package building or failing and have wait for
439 # long enough to have some package appearing in the ppa, exit
440 if (packages_not_in_ppa and not packages_building and
441@@ -143,6 +288,16 @@
442 logging.info(
443 "Some source packages were never published in the ppa: "
444 "{}".format(list_packages_info_in_str(packages_not_in_ppa)))
445+ for package in status:
446+ # If any packages are WAITING, set them to FAILED
447+ if package['status'] == SubTicketWorkflowStepStatus.WAITING.value:
448+ package['step'] = SubTicketWorkflowStep.COMPLETED.value,
449+ package['status'] = \
450+ SubTicketWorkflowStepStatus.PKG_BUILDING_COMPLETED.value
451+ # Add a message indicating the cause of the failure
452+ message_list.append(MISSING_SOURCE_MESSAGE.format(
453+ package['name'], package['version'], monitored_ppa.web_link))
454+ status['message'] = '\n'.join(message_list)
455 return (1, status)
456
457 # break out of status check loop if all packages have arrived in
458
459=== modified file 'juju-deployer/branch-source-builder.yaml.tmpl'
460--- juju-deployer/branch-source-builder.yaml.tmpl 2014-03-10 01:35:59 +0000
461+++ juju-deployer/branch-source-builder.yaml.tmpl 2014-03-10 22:57:01 +0000
462@@ -29,7 +29,7 @@
463 branch: ${CI_BRANCH}
464 tarball: ${CI_PAYLOAD_URL}
465 unit-config: include-base64://configs/unit_config.yaml
466- packages: "dput python-dput python-swiftclient"
467+ packages: "dput python-dput python-swiftclient lazr.enum"
468 install_sources: |
469 - ${CI_PPA}
470 install_keys: |

Subscribers

People subscribed via source and target branches