931
893
#use a url hack here, it is faster
932
894
person = '%s~%s' % (self.launchpad._root_uri,
933
self.options.get('triaging_team', 'ubuntu-crashes-universe'))
895
self.options.get('triaging_team', 'ubuntu-crashes-universe'))
934
896
bug.subscribe(person=person)
898
def _generate_upload_blob(self, report):
899
'''Generate a multipart/MIME temporary file for uploading.
901
You have to close the returned file object after you are done with it.
903
# set reprocessing tags
905
hdr['Tags'] = 'apport-%s' % report['ProblemType'].lower()
906
a = report.get('PackageArchitecture')
907
if not a or a == 'all':
908
a = report.get('Architecture')
910
hdr['Tags'] += ' ' + a
912
hdr['Tags'] += ' ' + report['Tags'].lower()
914
# privacy/retracing for distro reports
915
# FIXME: ugly hack until LP has a real crash db
916
if 'DistroRelease' in report:
917
if a and ('VmCore' in report or 'CoreDump' in report):
918
hdr['Private'] = 'yes'
919
hdr['Subscribers'] = self.options.get('initial_subscriber', 'apport')
920
hdr['Tags'] += ' need-%s-retrace' % a
921
elif 'Traceback' in report:
922
hdr['Private'] = 'yes'
923
hdr['Subscribers'] = 'apport'
924
hdr['Tags'] += ' need-duplicate-check'
925
if 'DuplicateSignature' in report and 'need-duplicate-check' not in hdr['Tags']:
926
hdr['Tags'] += ' need-duplicate-check'
928
# if we have checkbox submission key, link it to the bug; keep text
929
# reference until the link is shown in Launchpad's UI
930
if 'CheckboxSubmission' in report:
931
hdr['HWDB-Submission'] = report['CheckboxSubmission']
933
# order in which keys should appear in the temporary file
934
order = ['ProblemType', 'DistroRelease', 'Package', 'Regression', 'Reproducible',
935
'TestedUpstream', 'ProcVersionSignature', 'Uname', 'NonfreeKernelModules']
937
# write MIME/Multipart version into temporary file
938
mime = tempfile.TemporaryFile()
939
report.write_mime(mime, extra_headers=hdr, skip_keys=['Tags'], priority_fields=order)
937
946
# Launchpad storeblob API (should go into launchpadlib, see LP #315358)
1084
1091
summary=name + 'summary',
1085
1092
title=name + 'title')
1087
def _file_uncommon_description_bug(self):
1096
'''Get the Launchpad hostname for the given crashdb.'''
1098
return self.crashdb.get_hostname()
1100
def get_segv_report(self, force_fresh=False):
1101
'''Generate SEGV crash report.
1103
This is only done once, subsequent calls will return the already
1104
existing ID, unless force_fresh is True.
1109
if not force_fresh and _segv_report is not None:
1112
r = self._generate_sigsegv_report()
1113
r.add_package_info(self.test_package)
1117
self.assertEqual(r.standard_title(), 'crash crashed with SIGSEGV in f()')
1119
# add some binary gibberish which isn't UTF-8
1120
r['ShortGibberish'] = ' "]\xb6"\n'
1121
r['LongGibberish'] = 'a\nb\nc\nd\ne\n\xff\xff\xff\n\f'
1123
# create a bug for the report
1124
bug_target = self._get_bug_target(self.crashdb, r)
1125
self.assertTrue(bug_target)
1127
id = self._file_bug(bug_target, r)
1128
self.assertTrue(id > 0)
1130
sys.stderr.write('(Created SEGV report: https://%s/bugs/%i) ' % (self.hostname, id))
1135
def get_python_report(self):
1136
'''Generate Python crash report.
1140
global _python_report
1141
if _python_report is not None:
1142
return _python_report
1144
r = apport.Report('Crash')
1145
r['ExecutablePath'] = '/bin/foo'
1146
r['Traceback'] = '''Traceback (most recent call last):
1147
File "/bin/foo", line 67, in fuzz
1149
NameError: global name 'weird' is not defined'''
1150
r['Tags'] = 'boogus pybogus'
1151
r.add_package_info(self.test_package)
1154
self.assertEqual(r.standard_title(),
1155
"foo crashed with NameError in fuzz(): global name 'weird' is not defined")
1157
bug_target = self._get_bug_target(self.crashdb, r)
1158
self.assertTrue(bug_target)
1160
id = self._file_bug(bug_target, r)
1161
self.assertTrue(id > 0)
1162
sys.stderr.write('(Created Python report: https://%s/bugs/%i) ' % (self.hostname, id))
1166
def get_uncommon_description_report(self, force_fresh=False):
1088
1167
'''File a bug report with an uncommon description.
1169
This is only done once, subsequent calls will return the already
1170
existing ID, unless force_fresh is True.
1090
1172
Example taken from real LP bug 269539. It contains only
1091
1173
ProblemType/Architecture/DistroRelease in the description, and has
1092
1174
free-form description text after the Apport data.
1178
global _uncommon_description_report
1179
if not force_fresh and _uncommon_description_report is not None:
1180
return _uncommon_description_report
1094
1182
desc = '''problem
1096
1184
ProblemType: Package
1104
return self.crashdb.launchpad.bugs.createBug(
1192
bug = self.crashdb.launchpad.bugs.createBug(
1105
1193
title=b'mixed description bug'.encode(),
1106
1194
description=desc,
1107
1195
target=self.crashdb.lp_distro)
1111
'''Get the Launchpad hostname for the given crashdb.'''
1113
return self.crashdb.get_hostname()
1115
def _file_segv_report(self):
1116
'''File a SEGV crash report.
1118
Return (crash ID, report).
1120
r = self._generate_sigsegv_report()
1121
r.add_package_info(self.test_package)
1125
self.assertEqual(r.standard_title(), 'crash crashed with SIGSEGV in f()')
1127
# add some binary gibberish which isn't UTF-8
1128
r['ShortGibberish'] = ' "]\xb6"\n'
1129
r['LongGibberish'] = 'a\nb\nc\nd\ne\n\xff\xff\xff\n\f'
1131
handle = self.crashdb.upload(r)
1132
self.assertTrue(handle)
1133
bug_target = self._get_bug_target(self.crashdb, r)
1134
self.assertTrue(bug_target)
1136
id = self._file_bug(bug_target, r, handle)
1137
self.assertTrue(id > 0)
1140
def test_1_report_segv(self):
1141
'''upload() and get_comment_url() for SEGV crash
1143
This needs to run first, since it sets segv_report.
1146
(id, report) = self._file_segv_report()
1148
url = self.crashdb.get_comment_url(report, id)
1150
sys.stderr.write('(https://%s/bugs/%i) ' % (self.hostname, id))
1152
#TODO: check this programatically
1153
sys.stderr.write('[%s] ' % url)
1155
def test_1_report_python(self):
1156
'''upload() and get_comment_url() for Python crash
1158
This needs to run early, since it sets python_report.
1160
r = apport.Report('Crash')
1161
r['ExecutablePath'] = '/bin/foo'
1162
r['Traceback'] = '''Traceback (most recent call last):
1163
File "/bin/foo", line 67, in fuzz
1165
NameError: global name 'weird' is not defined'''
1166
r['Tags'] = 'boogus pybogus'
1167
r.add_package_info(self.test_package)
1170
self.assertEqual(r.standard_title(),
1171
"foo crashed with NameError in fuzz(): global name 'weird' is not defined")
1173
handle = self.crashdb.upload(r)
1174
self.assertTrue(handle)
1175
bug_target = self._get_bug_target(self.crashdb, r)
1176
self.assertTrue(bug_target)
1178
id = self._file_bug(bug_target, r, handle)
1179
self.assertTrue(id > 0)
1180
global python_report
1182
sys.stderr.write('(https://%s/bugs/%i) ' % (self.hostname, id))
1184
def test_2_download(self):
1196
sys.stderr.write('(Created uncommon description: https://%s/bugs/%i) ' % (self.hostname, bug.id))
1199
_uncommon_description_report = bug.id
1202
def test_1_download(self):
1185
1203
'''download()'''
1187
r = self.crashdb.download(segv_report)
1205
r = self.crashdb.download(self.get_segv_report())
1188
1206
self.assertEqual(r['ProblemType'], 'Crash')
1189
1207
self.assertEqual(r['Title'], 'crash crashed with SIGSEGV in f()')
1190
1208
self.assertEqual(r['DistroRelease'], self.ref_report['DistroRelease'])
1191
1209
self.assertEqual(r['Architecture'], self.ref_report['Architecture'])
1192
1210
self.assertEqual(r['Uname'], self.ref_report['Uname'])
1193
1211
self.assertEqual(r.get('NonfreeKernelModules'),
1194
self.ref_report.get('NonfreeKernelModules'))
1212
self.ref_report.get('NonfreeKernelModules'))
1195
1213
self.assertEqual(r.get('UserGroups'), self.ref_report.get('UserGroups'))
1196
1214
tags = set(r['Tags'].split())
1197
1215
self.assertEqual(tags, set([self.crashdb.arch_tag, 'apport-crash',
1198
apport.packaging.get_system_architecture()]))
1216
apport.packaging.get_system_architecture()]))
1200
1218
self.assertEqual(r['Signal'], '11')
1201
1219
self.assertTrue(r['ExecutablePath'].endswith('/crash'))
1421
1439
def test_get_distro_release(self):
1422
1440
'''get_distro_release()'''
1424
self.assertEqual(self.crashdb.get_distro_release(segv_report),
1425
self.ref_report['DistroRelease'])
1442
self.assertEqual(self.crashdb.get_distro_release(self.get_segv_report()),
1443
self.ref_report['DistroRelease'])
1427
1445
def test_get_affected_packages(self):
1428
1446
'''get_affected_packages()'''
1430
self.assertEqual(self.crashdb.get_affected_packages(segv_report),
1431
[self.ref_report['SourcePackage']])
1448
self.assertEqual(self.crashdb.get_affected_packages(self.get_segv_report()),
1449
[self.ref_report['SourcePackage']])
1433
1451
def test_is_reporter(self):
1434
1452
'''is_reporter()'''
1436
self.assertTrue(self.crashdb.is_reporter(segv_report))
1454
self.assertTrue(self.crashdb.is_reporter(self.get_segv_report()))
1437
1455
self.assertFalse(self.crashdb.is_reporter(1))
1439
1457
def test_can_update(self):
1440
1458
'''can_update()'''
1442
self.assertTrue(self.crashdb.can_update(segv_report))
1460
self.assertTrue(self.crashdb.can_update(self.get_segv_report()))
1443
1461
self.assertFalse(self.crashdb.can_update(1))
1445
1463
def test_duplicates(self):
1446
1464
'''duplicate handling'''
1448
1466
# initially we have no dups
1449
self.assertEqual(self.crashdb.duplicate_of(segv_report), None)
1450
self.assertEqual(self.crashdb.get_fixed_version(segv_report), None)
1467
self.assertEqual(self.crashdb.duplicate_of(self.get_segv_report()), None)
1468
self.assertEqual(self.crashdb.get_fixed_version(self.get_segv_report()), None)
1470
segv_id = self.get_segv_report()
1471
known_test_id = self.get_uncommon_description_report()
1472
known_test_id2 = self.get_uncommon_description_report(force_fresh=True)
1452
1474
# dupe our segv_report and check that it worked; then undupe it
1453
r = self.crashdb.download(segv_report)
1454
self.crashdb.close_duplicate(r, segv_report, self.known_test_id)
1455
self.assertEqual(self.crashdb.duplicate_of(segv_report), self.known_test_id)
1475
r = self.crashdb.download(segv_id)
1476
self.crashdb.close_duplicate(r, segv_id, known_test_id)
1477
self.assertEqual(self.crashdb.duplicate_of(segv_id), known_test_id)
1457
1479
# this should be a no-op
1458
self.crashdb.close_duplicate(r, segv_report, self.known_test_id)
1459
self.assertEqual(self.crashdb.duplicate_of(segv_report), self.known_test_id)
1480
self.crashdb.close_duplicate(r, segv_id, known_test_id)
1481
self.assertEqual(self.crashdb.duplicate_of(segv_id), known_test_id)
1461
self.assertEqual(self.crashdb.get_fixed_version(segv_report), 'invalid')
1462
self.crashdb.close_duplicate(r, segv_report, None)
1463
self.assertEqual(self.crashdb.duplicate_of(segv_report), None)
1464
self.assertEqual(self.crashdb.get_fixed_version(segv_report), None)
1483
self.assertEqual(self.crashdb.get_fixed_version(segv_id), 'invalid')
1484
self.crashdb.close_duplicate(r, segv_id, None)
1485
self.assertEqual(self.crashdb.duplicate_of(segv_id), None)
1486
self.assertEqual(self.crashdb.get_fixed_version(segv_id), None)
1466
1488
# this should have removed attachments; note that Stacktrace is
1467
1489
# short, and thus inline
1468
r = self.crashdb.download(segv_report)
1490
r = self.crashdb.download(self.get_segv_report())
1469
1491
self.assertFalse('CoreDump' in r)
1470
1492
self.assertFalse('Disassembly' in r)
1471
1493
self.assertFalse('ProcMaps' in r)
1472
1494
self.assertFalse('ProcStatus' in r)
1473
1495
self.assertFalse('Registers' in r)
1474
self.assertFalse('Stacktrace' in r)
1475
1496
self.assertFalse('ThreadStacktrace' in r)
1477
1498
# now try duplicating to a duplicate bug; this should automatically
1478
1499
# transition to the master bug
1479
self.crashdb.close_duplicate(apport.Report(), self.known_test_id,
1480
self.known_test_id2)
1481
self.crashdb.close_duplicate(r, segv_report, self.known_test_id)
1482
self.assertEqual(self.crashdb.duplicate_of(segv_report),
1483
self.known_test_id2)
1500
self.crashdb.close_duplicate(apport.Report(), known_test_id,
1502
self.crashdb.close_duplicate(r, segv_id, known_test_id)
1503
self.assertEqual(self.crashdb.duplicate_of(segv_id),
1485
self.crashdb.close_duplicate(apport.Report(), self.known_test_id, None)
1486
self.crashdb.close_duplicate(apport.Report(), self.known_test_id2, None)
1487
self.crashdb.close_duplicate(r, segv_report, None)
1506
self.crashdb.close_duplicate(apport.Report(), known_test_id, None)
1507
self.crashdb.close_duplicate(apport.Report(), known_test_id2, None)
1508
self.crashdb.close_duplicate(r, segv_id, None)
1489
1510
# this should be a no-op
1490
self.crashdb.close_duplicate(apport.Report(), self.known_test_id, None)
1491
self.assertEqual(self.crashdb.duplicate_of(self.known_test_id), None)
1511
self.crashdb.close_duplicate(apport.Report(), known_test_id, None)
1512
self.assertEqual(self.crashdb.duplicate_of(known_test_id), None)
1493
self.crashdb.mark_regression(segv_report, self.known_test_id)
1494
self._verify_marked_regression(segv_report)
1514
self.crashdb.mark_regression(segv_id, known_test_id)
1515
self._verify_marked_regression(segv_id)
1496
1517
def test_marking_segv(self):
1497
1518
'''processing status markings for signal crashes'''
1499
1520
# mark_retraced()
1500
1521
unretraced_before = self.crashdb.get_unretraced()
1501
self.assertTrue(segv_report in unretraced_before)
1502
self.assertFalse(python_report in unretraced_before)
1503
self.crashdb.mark_retraced(segv_report)
1522
self.assertTrue(self.get_segv_report() in unretraced_before)
1523
self.assertFalse(self.get_python_report() in unretraced_before)
1524
self.crashdb.mark_retraced(self.get_segv_report())
1504
1525
unretraced_after = self.crashdb.get_unretraced()
1505
self.assertFalse(segv_report in unretraced_after)
1526
self.assertFalse(self.get_segv_report() in unretraced_after)
1506
1527
self.assertEqual(unretraced_before,
1507
unretraced_after.union(set([segv_report])))
1508
self.assertEqual(self.crashdb.get_fixed_version(segv_report), None)
1528
unretraced_after.union(set([self.get_segv_report()])))
1529
self.assertEqual(self.crashdb.get_fixed_version(self.get_segv_report()), None)
1510
1531
# mark_retrace_failed()
1511
self._mark_needs_retrace(segv_report)
1512
self.crashdb.mark_retraced(segv_report)
1513
self.crashdb.mark_retrace_failed(segv_report)
1532
self._mark_needs_retrace(self.get_segv_report())
1533
self.crashdb.mark_retraced(self.get_segv_report())
1534
self.crashdb.mark_retrace_failed(self.get_segv_report())
1514
1535
unretraced_after = self.crashdb.get_unretraced()
1515
self.assertFalse(segv_report in unretraced_after)
1536
self.assertFalse(self.get_segv_report() in unretraced_after)
1516
1537
self.assertEqual(unretraced_before,
1517
unretraced_after.union(set([segv_report])))
1518
self.assertEqual(self.crashdb.get_fixed_version(segv_report), None)
1538
unretraced_after.union(set([self.get_segv_report()])))
1539
self.assertEqual(self.crashdb.get_fixed_version(self.get_segv_report()), None)
1520
1541
# mark_retrace_failed() of invalid bug
1521
self._mark_needs_retrace(segv_report)
1522
self.crashdb.mark_retraced(segv_report)
1523
self.crashdb.mark_retrace_failed(segv_report, "I don't like you")
1542
self._mark_needs_retrace(self.get_segv_report())
1543
self.crashdb.mark_retraced(self.get_segv_report())
1544
self.crashdb.mark_retrace_failed(self.get_segv_report(), "I don't like you")
1524
1545
unretraced_after = self.crashdb.get_unretraced()
1525
self.assertFalse(segv_report in unretraced_after)
1526
self.assertEqual(unretraced_before,
1527
unretraced_after.union(set([segv_report])))
1528
self.assertEqual(self.crashdb.get_fixed_version(segv_report),
1546
self.assertFalse(self.get_segv_report() in unretraced_after)
1547
self.assertEqual(unretraced_before,
1548
unretraced_after.union(set([self.get_segv_report()])))
1549
self.assertEqual(self.crashdb.get_fixed_version(self.get_segv_report()),
1552
def test_marking_project(self):
1553
'''processing status markings for a project CrashDB'''
1555
# create a distro bug
1556
distro_bug = self.crashdb.launchpad.bugs.createBug(
1558
tags=self.crashdb.arch_tag,
1559
target=self.crashdb.lp_distro,
1560
title='ubuntu distro retrace bug')
1561
#print('distro bug: https://staging.launchpad.net/bugs/%i' % distro_bug.id)
1563
# create a project crash DB and a bug
1564
launchpad_instance = os.environ.get('APPORT_LAUNCHPAD_INSTANCE') or 'staging'
1566
project_db = CrashDatabase(
1567
os.environ.get('LP_CREDENTIALS'),
1568
{'project': 'langpack-o-matic', 'launchpad_instance': launchpad_instance})
1569
project_bug = project_db.launchpad.bugs.createBug(
1571
tags=project_db.arch_tag,
1572
target=project_db.lp_distro,
1573
title='project retrace bug')
1574
#print('project bug: https://staging.launchpad.net/bugs/%i' % project_bug.id)
1576
# on project_db, we recognize the project bug and can mark it
1577
unretraced_before = project_db.get_unretraced()
1578
self.assertTrue(project_bug.id in unretraced_before)
1579
self.assertFalse(distro_bug.id in unretraced_before)
1580
project_db.mark_retraced(project_bug.id)
1581
unretraced_after = project_db.get_unretraced()
1582
self.assertFalse(project_bug.id in unretraced_after)
1583
self.assertEqual(unretraced_before,
1584
unretraced_after.union(set([project_bug.id])))
1585
self.assertEqual(self.crashdb.get_fixed_version(project_bug.id), None)
1531
1587
def test_marking_python(self):
1532
1588
'''processing status markings for interpreter crashes'''
1534
1590
unchecked_before = self.crashdb.get_dup_unchecked()
1535
self.assertTrue(python_report in unchecked_before)
1536
self.assertFalse(segv_report in unchecked_before)
1537
self.crashdb._mark_dup_checked(python_report, self.ref_report)
1591
self.assertTrue(self.get_python_report() in unchecked_before)
1592
self.assertFalse(self.get_segv_report() in unchecked_before)
1593
self.crashdb._mark_dup_checked(self.get_python_report(), self.ref_report)
1538
1594
unchecked_after = self.crashdb.get_dup_unchecked()
1539
self.assertFalse(python_report in unchecked_after)
1595
self.assertFalse(self.get_python_report() in unchecked_after)
1540
1596
self.assertEqual(unchecked_before,
1541
unchecked_after.union(set([python_report])))
1542
self.assertEqual(self.crashdb.get_fixed_version(python_report),
1597
unchecked_after.union(set([self.get_python_report()])))
1598
self.assertEqual(self.crashdb.get_fixed_version(self.get_python_report()), None)
1545
1600
def test_update_traces_invalid(self):
1546
1601
'''updating an invalid crash
1603
1660
return self.lp_distro
1605
def _get_librarian_hostname(self):
1606
'''Return the librarian hostname according to the LP hostname used.'''
1608
hostname = self.crashdb.get_hostname()
1609
if 'staging' in hostname:
1610
return 'staging.launchpadlibrarian.net'
1612
return 'launchpad.dev:58080'
1614
def _file_bug(self, bug_target, report, handle, comment=None):
1615
'''File a bug report.'''
1617
bug_title = report.get('Title', report.standard_title())
1619
blob_info = self.crashdb.launchpad.temporary_blobs.fetch(
1621
# XXX 2010-08-03 matsubara bug=612990:
1622
# Can't fetch the blob directly, so let's load it from the
1624
blob = self.crashdb.launchpad.load(blob_info['self_link'])
1625
#XXX Need to find a way to trigger the job that process the blob
1626
# rather polling like this. This makes the test suite take forever
1628
while not blob.hasBeenProcessed():
1631
# processed_blob contains info about privacy, additional comments
1633
processed_blob = blob.getProcessedData()
1662
def _file_bug(self, bug_target, report, description=None):
1663
'''File a bug report for a report.
1667
# unfortunately staging's +storeblob API hardly ever works, so we
1668
# must avoid using it. Fake it by manually doing the comments and
1669
# attachments that +filebug would ordinarily do itself when given a
1672
if description is None:
1673
description = 'some description'
1675
mime = self.crashdb._generate_upload_blob(report)
1676
msg = email.message_from_file(mime)
1678
msg_iter = msg.walk()
1680
# first one is the multipart container
1681
header = msg_iter.next()
1682
assert header.is_multipart()
1684
# second part should be an inline text/plain attachments with all short
1686
part = msg_iter.next()
1687
assert not part.is_multipart()
1688
assert part.get_content_type() == 'text/plain'
1689
description += '\n\n' + part.get_payload(decode=True).decode('UTF-8', 'replace')
1691
# create the bug from header and description data
1635
1692
bug = self.crashdb.launchpad.bugs.createBug(
1636
description=processed_blob['extra_description'],
1637
private=processed_blob['private'],
1638
tags=processed_blob['initial_tags'],
1693
description=description,
1694
private=(header['Private'] == 'yes'),
1695
tags=header['Tags'].split(),
1639
1696
target=bug_target,
1642
for comment in processed_blob['comments']:
1643
bug.newMessage(content=comment)
1645
# Ideally, one would be able to retrieve the attachment content
1646
# from the ProblemReport object or from the processed_blob.
1647
# Unfortunately the processed_blob only give us the Launchpad
1648
# librarian file_alias_id, so that's why we need to
1649
# download it again and upload to the bug report. It'd be even
1650
# better if addAttachment could work like linkAttachment, the LP
1651
# api used in the +filebug web UI, but there are security concerns
1652
# about the way linkAttachment works.
1653
librarian_url = 'http://%s' % self._get_librarian_hostname()
1654
for attachment in processed_blob['attachments']:
1655
filename = description = attachment['description']
1656
# Download the attachment data.
1657
data = urlopen(urllib.basejoin(librarian_url,
1658
str(attachment['file_alias_id']) + '/' + filename)).read()
1659
# Add the attachment to the newly created bug report.
1664
description=description)
1666
for subscriber in processed_blob['subscribers']:
1697
title=report.get('Title', report.standard_title()))
1699
# nwo add the attachments
1700
for part in msg_iter:
1701
assert not part.is_multipart()
1702
bug.addAttachment(comment='',
1703
description=part.get_filename(),
1705
data=part.get_payload(decode=True),
1706
filename=part.get_filename(), is_patch=False)
1708
for subscriber in header['Subscribers'].split():
1667
1709
sub = self.crashdb.launchpad.people[subscriber]
1669
1711
bug.subscribe(person=sub)
1671
for submission_key in processed_blob['hwdb_submission_keys']:
1672
# XXX 2010-08-04 matsubara bug=628889:
1673
# Can't fetch the submission directly, so let's load it
1674
# from the representation.
1675
submission = self.crashdb.launchpad.load(
1676
'https://api.%s/beta/+hwdb/+submission/%s'
1677
% (self.crashdb.get_hostname(), submission_key))
1678
bug.linkHWSubmission(submission=submission)
1681
1715
def _mark_needs_retrace(self, id):
1682
1716
'''Mark a report ID as needing retrace.'''