tracker_tasks.py 94 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480148114821483148414851486148714881489149014911492149314941495149614971498149915001501150215031504150515061507150815091510151115121513151415151516151715181519152015211522152315241525152615271528152915301531153215331534153515361537153815391540154115421543154415451546154715481549155015511552155315541555155615571558155915601561156215631564156515661567156815691570157115721573157415751576157715781579158015811582158315841585158615871588158915901591159215931594159515961597159815991600160116021603160416051606160716081609161016111612161316141615161616171618161916201621162216231624162516261627162816291630163116321633163416351636163716381639164016411642164316441645164616471648164916501651165216531654165516561657165816591660166116621663166416651666166716681669167016711672167316741675167616771678167916801681168216831684168516861687168816891690169116921693169416951696169716981699170017011702170317041705170617071708170917101711171217131714171517161717171817191720172117221723172417251726172717281729173017311732173317341735173617371738173917401741174217431744174517461747174817491750175117521753175417551756175717581759176017611762176317641765176617671768176917701771177217731774177517761777177817791780178117821783178417851786178717881789179017911792179317941795179617971798179918001801180218031804180518061807180818091810181118121813181418151816181718181819182018211822182318241825182618271828182918301831183218331834183518361837183818391840184118421843184418451846184718481849185018511852185318541855185618571858185918601861186218631864186518661867186818691870187118721873187418751876187718781879188018811882188318841885188618871888188918901891189218931894189518961897189818991900190119021903190419051906190719081909191019111912191319141915191619171918191919201921192219231924192519261927192819291930193119321933193419351936193719381939194019411942194319441945194619471948194919501951195219531954195519561957195819591960196119621963196419651966196719681969197019711972197319741975197619771978197919801981198219831984198519861987198819891990199119921993199419951996199719981999200020012002200320042005200620072008200920102011201220132014201520162017201820192020202120222023202420252026202720282029203020312032203320342035203620372038203920402041204220432044204520462047204820492050205120522053205420552056205720582059206020612062206320642065206620672068206920702071207220732074207520762077207820792080208120822083208420852086208720882089209020912092209320942095209620972098209921002101210221032104210521062107210821092110211121122113211421152116211721182119212021212122212321242125212621272128212921302131213221332134213521362137213821392140214121422143214421452146214721482149215021512152215321542155215621572158215921602161216221632164216521662167216821692170217121722173217421752176217721782179218021812182218321842185218621872188218921902191219221932194219521962197219821992200220122022203220422052206220722082209221022112212221322142215221622172218221922202221222222232224222522262227222822292230223122322233223422352236223722382239224022412242224322442245224622472248224922502251225222532254225522562257225822592260226122622263226422652266226722682269227022712272227322742275227622772278227922802281228222832284228522862287228822892290229122922293229422952296229722982299230023012302230323042305230623072308230923102311231223132314231523162317231823192320232123222323232423252326232723282329233023312332233323342335233623372338233923402341234223432344234523462347234823492350235123522353235423552356235723582359236023612362236323642365236623672368236923702371237223732374237523762377237823792380238123822383238423852386238723882389239023912392239323942395239623972398239924002401240224032404240524062407240824092410241124122413241424152416241724182419242024212422242324242425242624272428242924302431243224332434243524362437243824392440244124422443244424452446244724482449245024512452
  1. # Copyright 2013 The Distro Tracker Developers
  2. # See the COPYRIGHT file at the top-level directory of this distribution and
  3. # at http://deb.li/DTAuthors
  4. #
  5. # This file is part of Distro Tracker. It is subject to the license terms
  6. # in the LICENSE file found in the top-level directory of this
  7. # distribution and at http://deb.li/DTLicense. No part of Distro Tracker,
  8. # including this file, may be copied, modified, propagated, or distributed
  9. # except according to the terms contained in the LICENSE file.
  10. """
  11. Debian-specific tasks.
  12. """
  13. from __future__ import unicode_literals
  14. from django.db import transaction
  15. from django.conf import settings
  16. from django.utils import six
  17. from django.utils.encoding import force_str
  18. from django.utils.http import urlencode
  19. from django.core.urlresolvers import reverse
  20. from distro_tracker.core.tasks import BaseTask
  21. from distro_tracker.core.models import PackageExtractedInfo
  22. from distro_tracker.core.models import ActionItem, ActionItemType
  23. from distro_tracker.accounts.models import UserEmail
  24. from distro_tracker.core.models import PackageBugStats
  25. from distro_tracker.core.models import BinaryPackageBugStats
  26. from distro_tracker.core.models import PackageName
  27. from distro_tracker.core.models import SourcePackageName
  28. from distro_tracker.core.models import BinaryPackageName
  29. from distro_tracker.core.models import SourcePackageDeps
  30. from distro_tracker.vendor.debian.models import LintianStats
  31. from distro_tracker.vendor.debian.models import BuildLogCheckStats
  32. from distro_tracker.vendor.debian.models import PackageTransition
  33. from distro_tracker.vendor.debian.models import PackageExcuses
  34. from distro_tracker.vendor.debian.models import UbuntuPackage
  35. from distro_tracker.core.utils.http import HttpCache
  36. from distro_tracker.core.utils.http import get_resource_content
  37. from distro_tracker.core.utils.packages import package_hashdir
  38. from .models import DebianContributor
  39. from distro_tracker import vendor
  40. import collections
  41. import os
  42. import re
  43. import json
  44. import hashlib
  45. import itertools
  46. from debian import deb822
  47. from debian.debian_support import AptPkgVersion
  48. from debian import debian_support
  49. from copy import deepcopy
  50. from bs4 import BeautifulSoup as soup
  51. import yaml
  52. try:
  53. import SOAPpy
  54. except ImportError:
  55. pass
  56. import logging
  57. logger = logging.getLogger(__name__)
  58. class RetrieveDebianMaintainersTask(BaseTask):
  59. """
  60. Retrieves (and updates if necessary) a list of Debian Maintainers.
  61. """
  62. def __init__(self, force_update=False, *args, **kwargs):
  63. super(RetrieveDebianMaintainersTask, self).__init__(*args, **kwargs)
  64. self.force_update = force_update
  65. def set_parameters(self, parameters):
  66. if 'force_update' in parameters:
  67. self.force_update = parameters['force_update']
  68. def execute(self):
  69. cache = HttpCache(settings.DISTRO_TRACKER_CACHE_DIRECTORY)
  70. url = "https://ftp-master.debian.org/dm.txt"
  71. if not self.force_update and not cache.is_expired(url):
  72. # No need to do anything when the previously cached value is fresh
  73. return
  74. response, updated = cache.update(url, force=self.force_update)
  75. response.raise_for_status()
  76. if not updated:
  77. # No need to do anything if the cached item was still not updated
  78. return
  79. maintainers = {}
  80. lines = response.iter_lines(decode_unicode=True)
  81. for stanza in deb822.Deb822.iter_paragraphs(lines):
  82. if 'Uid' in stanza and 'Allow' in stanza:
  83. # Allow is a comma-separated string of 'package (DD fpr)' items,
  84. # where DD fpr is the fingerprint of the DD that granted the
  85. # permission
  86. name, email = stanza['Uid'].rsplit(' ', 1)
  87. email = email.strip('<>')
  88. for pair in stanza['Allow'].split(','):
  89. pair = pair.strip()
  90. pkg, dd_fpr = pair.split()
  91. maintainers.setdefault(email, [])
  92. maintainers[email].append(pkg)
  93. # Now update the developer information
  94. with transaction.atomic():
  95. # Reset all old maintainers first.
  96. qs = DebianContributor.objects.filter(is_debian_maintainer=True)
  97. qs.update(is_debian_maintainer=False)
  98. for email, packages in maintainers.items():
  99. email, _ = UserEmail.objects.get_or_create(email=email)
  100. contributor, _ = DebianContributor.objects.get_or_create(
  101. email=email)
  102. contributor.is_debian_maintainer = True
  103. contributor.allowed_packages = packages
  104. contributor.save()
  105. class RetrieveLowThresholdNmuTask(BaseTask):
  106. """
  107. Updates the list of Debian Maintainers which agree with the lowthreshold
  108. NMU.
  109. """
  110. def __init__(self, force_update=False, *args, **kwargs):
  111. super(RetrieveLowThresholdNmuTask, self).__init__(*args, **kwargs)
  112. self.force_update = force_update
  113. def set_parameters(self, parameters):
  114. if 'force_update' in parameters:
  115. self.force_update = parameters['force_update']
  116. def _retrieve_emails(self):
  117. """
  118. Helper function which obtains the list of emails of maintainers that
  119. agree with the lowthreshold NMU.
  120. """
  121. url = 'https://wiki.debian.org/LowThresholdNmu?action=raw'
  122. cache = HttpCache(settings.DISTRO_TRACKER_CACHE_DIRECTORY)
  123. if not self.force_update and not cache.is_expired(url):
  124. return
  125. response, updated = cache.update(url, force=self.force_update)
  126. response.raise_for_status()
  127. if not updated:
  128. return
  129. emails = []
  130. devel_php_RE = re.compile(
  131. r'https?://qa\.debian\.org/developer\.php\?login=([^\s&|]+)')
  132. word_RE = re.compile(r'^\w+$')
  133. for line in response.iter_lines():
  134. match = devel_php_RE.search(line)
  135. while match: # look for several matches on the same line
  136. email = None
  137. login = match.group(1)
  138. if word_RE.match(login):
  139. email = login + '@debian.org'
  140. elif login.find('@') >= 0:
  141. email = login
  142. if email:
  143. emails.append(email)
  144. line = line[match.end():]
  145. match = devel_php_RE.search(line)
  146. return emails
  147. def execute(self):
  148. emails = self._retrieve_emails()
  149. with transaction.atomic():
  150. # Reset all threshold flags first.
  151. qs = DebianContributor.objects.filter(
  152. agree_with_low_threshold_nmu=True)
  153. qs.update(agree_with_low_threshold_nmu=False)
  154. for email in emails:
  155. email, _ = UserEmail.objects.get_or_create(email=email)
  156. contributor, _ = DebianContributor.objects.get_or_create(
  157. email=email)
  158. contributor.agree_with_low_threshold_nmu = True
  159. contributor.save()
  160. class UpdatePackageBugStats(BaseTask):
  161. """
  162. Updates the BTS bug stats for all packages (source, binary and pseudo).
  163. Creates :class:`distro_tracker.core.ActionItem` instances for packages
  164. which have bugs tagged help or patch.
  165. """
  166. PATCH_BUG_ACTION_ITEM_TYPE_NAME = 'debian-patch-bugs-warning'
  167. HELP_BUG_ACTION_ITEM_TYPE_NAME = 'debian-help-bugs-warning'
  168. PATCH_ITEM_SHORT_DESCRIPTION = (
  169. '<a href="{url}">{count}</a> tagged patch in the '
  170. '<abbr title="Bug Tracking System">BTS</abbr>')
  171. HELP_ITEM_SHORT_DESCRIPTION = (
  172. '<a href="{url}">{count}</a> tagged help in the '
  173. '<abbr title="Bug Tracking System">BTS</abbr>')
  174. PATCH_ITEM_FULL_DESCRIPTION_TEMPLATE = 'debian/patch-bugs-action-item.html'
  175. HELP_ITEM_FULL_DESCRIPTION_TEMPLATE = 'debian/help-bugs-action-item.html'
  176. bug_categories = (
  177. 'rc',
  178. 'normal',
  179. 'wishlist',
  180. 'fixed',
  181. 'patch',
  182. )
  183. def __init__(self, force_update=False, *args, **kwargs):
  184. super(UpdatePackageBugStats, self).__init__(*args, **kwargs)
  185. self.force_update = force_update
  186. self.cache = HttpCache(settings.DISTRO_TRACKER_CACHE_DIRECTORY)
  187. # The :class:`distro_tracker.core.models.ActionItemType` instances which
  188. # this task can create.
  189. self.patch_item_type = ActionItemType.objects.create_or_update(
  190. type_name=self.PATCH_BUG_ACTION_ITEM_TYPE_NAME,
  191. full_description_template=self.PATCH_ITEM_FULL_DESCRIPTION_TEMPLATE)
  192. self.help_item_type = ActionItemType.objects.create_or_update(
  193. type_name=self.HELP_BUG_ACTION_ITEM_TYPE_NAME,
  194. full_description_template=self.HELP_ITEM_FULL_DESCRIPTION_TEMPLATE)
  195. def _get_tagged_bug_stats(self, tag, user=None):
  196. """
  197. Using the BTS SOAP interface, retrieves the statistics of bugs with a
  198. particular tag.
  199. :param tag: The tag for which the statistics are required.
  200. :type tag: string
  201. :param user: The email of the user who tagged the bug with the given
  202. tag.
  203. :type user: string
  204. :returns: A dict mapping package names to the count of bugs with the
  205. given tag.
  206. """
  207. debian_ca_bundle = '/etc/ssl/ca-debian/ca-certificates.crt'
  208. if os.path.exists(debian_ca_bundle):
  209. os.environ['SSL_CERT_FILE'] = debian_ca_bundle
  210. url = 'https://bugs.debian.org/cgi-bin/soap.cgi'
  211. namespace = 'Debbugs/SOAP'
  212. server = SOAPpy.SOAPProxy(url, namespace)
  213. if user:
  214. bugs = server.get_usertag(user, tag)
  215. bugs = bugs[0]
  216. else:
  217. bugs = server.get_bugs('tag', tag)
  218. # Match each retrieved bug ID to a package and then find the aggregate
  219. # count for each package.
  220. bug_stats = {}
  221. statuses = server.get_status(bugs)
  222. statuses = statuses[0]
  223. for status in statuses:
  224. status = status['value']
  225. if status['done'] or status['fixed'] or \
  226. status['pending'] == 'fixed':
  227. continue
  228. package_name = status['package']
  229. bug_stats.setdefault(package_name, 0)
  230. bug_stats[package_name] += 1
  231. return bug_stats
  232. def _extend_bug_stats(self, bug_stats, extra_stats, category_name):
  233. """
  234. Helper method which adds extra bug stats to an already existing list of
  235. stats.
  236. :param bug_stats: An already existing list of bug stats. Maps package
  237. names to list of bug category descriptions.
  238. :type bug_stats: dict
  239. :param extra_stats: Extra bug stats which should be added to
  240. ``bug_stats``. Maps package names to integers representing bug
  241. counts.
  242. :type extra_stats: dict
  243. :param category_name: The name of the bug category which is being added
  244. :type category_name: string
  245. """
  246. for package, count in extra_stats.items():
  247. bug_stats.setdefault(package, [])
  248. bug_stats[package].append({
  249. 'category_name': category_name,
  250. 'bug_count': count,
  251. })
  252. def _create_patch_bug_action_item(self, package, bug_stats):
  253. """
  254. Creates a :class:`distro_tracker.core.models.ActionItem` instance for
  255. the given package if it contains any bugs tagged patch.
  256. :param package: The package for which the action item should be
  257. updated.
  258. :type package: :class:`distro_tracker.core.models.PackageName`
  259. :param bug_stats: A dictionary mapping category names to structures
  260. describing those categories. Those structures should be
  261. identical to the ones stored in the :class:`PackageBugStats`
  262. instance.
  263. :type bug_stats: dict
  264. """
  265. # Get the old action item, if any
  266. action_item = package.get_action_item_for_type(
  267. self.PATCH_BUG_ACTION_ITEM_TYPE_NAME)
  268. if 'patch' not in bug_stats or bug_stats['patch']['bug_count'] == 0:
  269. # Remove the old action item, since the package does not have any
  270. # bugs tagged patch anymore.
  271. if action_item is not None:
  272. action_item.delete()
  273. return
  274. # If the package has bugs tagged patch, update the action item
  275. if action_item is None:
  276. action_item = ActionItem(
  277. package=package,
  278. item_type=self.patch_item_type)
  279. bug_count = bug_stats['patch']['bug_count']
  280. # Include the URL in the short description
  281. url, _ = vendor.call('get_bug_tracker_url', package.name, 'source',
  282. 'patch')
  283. if not url:
  284. url = ''
  285. # Include the bug count in the short description
  286. count = '{bug_count} bug'.format(bug_count=bug_count)
  287. if bug_count > 1:
  288. count += 's'
  289. action_item.short_description = \
  290. self.PATCH_ITEM_SHORT_DESCRIPTION.format(url=url, count=count)
  291. # Set additional URLs and merged bug count in the extra data for a full
  292. # description
  293. action_item.extra_data = {
  294. 'bug_count': bug_count,
  295. 'merged_count': bug_stats['patch'].get('merged_count', 0),
  296. 'url': url,
  297. 'merged_url': vendor.call(
  298. 'get_bug_tracker_url', package.name, 'source',
  299. 'patch-merged')[0],
  300. }
  301. action_item.save()
  302. def _create_help_bug_action_item(self, package, bug_stats):
  303. """
  304. Creates a :class:`distro_tracker.core.models.ActionItem` instance for
  305. the given package if it contains any bugs tagged help.
  306. :param package: The package for which the action item should be
  307. updated.
  308. :type package: :class:`distro_tracker.core.models.PackageName`
  309. :param bug_stats: A dictionary mapping category names to structures
  310. describing those categories. Those structures should be
  311. identical to the ones stored in the :class:`PackageBugStats`
  312. instance.
  313. :type bug_stats: dict
  314. """
  315. # Get the old action item, if any
  316. action_item = package.get_action_item_for_type(
  317. self.HELP_BUG_ACTION_ITEM_TYPE_NAME)
  318. if 'help' not in bug_stats or bug_stats['help']['bug_count'] == 0:
  319. # Remove the old action item, since the package does not have any
  320. # bugs tagged patch anymore.
  321. if action_item is not None:
  322. action_item.delete()
  323. return
  324. # If the package has bugs tagged patch, update the action item
  325. if action_item is None:
  326. action_item = ActionItem(
  327. package=package,
  328. item_type=self.help_item_type)
  329. bug_count = bug_stats['help']['bug_count']
  330. # Include the URL in the short description
  331. url, _ = vendor.call('get_bug_tracker_url', package.name, 'source',
  332. 'help')
  333. if not url:
  334. url = ''
  335. # Include the bug count in the short description
  336. count = '{bug_count} bug'.format(bug_count=bug_count)
  337. if bug_count > 1:
  338. count += 's'
  339. action_item.short_description = self.HELP_ITEM_SHORT_DESCRIPTION.format(
  340. url=url, count=count)
  341. # Set additional URLs and merged bug count in the extra data for a full
  342. # description
  343. action_item.extra_data = {
  344. 'bug_count': bug_count,
  345. 'url': url,
  346. }
  347. action_item.save()
  348. def _create_action_items(self, package_bug_stats):
  349. """
  350. Method which creates a :class:`distro_tracker.core.models.ActionItem`
  351. instance for a package based on the given package stats.
  352. For now, an action item is created if the package either has bugs
  353. tagged as help or patch.
  354. """
  355. # Transform the bug stats to a structure easier to pass to functions
  356. # for particular bug-category action items.
  357. bug_stats = {
  358. category['category_name']: category
  359. for category in package_bug_stats.stats
  360. }
  361. package = package_bug_stats.package
  362. self._create_patch_bug_action_item(package, bug_stats)
  363. self._create_help_bug_action_item(package, bug_stats)
  364. def _get_udd_bug_stats(self):
  365. url = 'https://udd.debian.org/cgi-bin/ddpo-bugs.cgi'
  366. response_content = get_resource_content(url)
  367. if not response_content:
  368. return
  369. # Each line in the response should be bug stats for a single package
  370. bug_stats = {}
  371. for line in response_content.splitlines():
  372. line = line.decode('utf-8', 'ignore').strip()
  373. try:
  374. package_name, bug_counts = line, ''
  375. if line.startswith('src:'):
  376. src, package_name, bug_counts = line.split(':', 2)
  377. else:
  378. package_name, bug_counts = line.split(':', 1)
  379. # Merged counts are in parentheses so remove those before
  380. # splitting the numbers
  381. bug_counts = re.sub(r'[()]', ' ', bug_counts).split()
  382. bug_counts = [int(count) for count in bug_counts]
  383. except ValueError:
  384. logger.warning(
  385. 'Failed to parse bug information for {pkg}: {cnts}'.format(
  386. pkg=package_name, cnts=bug_counts), exc_info=1)
  387. continue
  388. # Match the extracted counts with category names
  389. bug_stats[package_name] = [
  390. {
  391. 'category_name': category_name,
  392. 'bug_count': bug_count,
  393. 'merged_count': merged_count,
  394. }
  395. for category_name, (bug_count, merged_count) in zip(
  396. self.bug_categories, zip(bug_counts[::2], bug_counts[1::2]))
  397. ]
  398. return bug_stats
  399. def _remove_obsolete_action_items(self, package_names):
  400. """
  401. Removes action items for packages which no longer have any bug stats.
  402. """
  403. ActionItem.objects.delete_obsolete_items(
  404. item_types=[self.patch_item_type, self.help_item_type],
  405. non_obsolete_packages=package_names)
  406. def update_source_and_pseudo_bugs(self):
  407. """
  408. Performs the update of bug statistics for source and pseudo packages.
  409. """
  410. # First get the bug stats exposed by the UDD.
  411. bug_stats = self._get_udd_bug_stats()
  412. if not bug_stats:
  413. bug_stats = {}
  414. # Add in help bugs from the BTS SOAP interface
  415. try:
  416. help_bugs = self._get_tagged_bug_stats('help')
  417. self._extend_bug_stats(bug_stats, help_bugs, 'help')
  418. except:
  419. logger.exception("Could not get bugs tagged help")
  420. # Add in newcomer bugs from the BTS SOAP interface
  421. try:
  422. newcomer_bugs = self._get_tagged_bug_stats('newcomer')
  423. self._extend_bug_stats(bug_stats, newcomer_bugs, 'newcomer')
  424. except:
  425. logger.exception("Could not get bugs tagged newcomer")
  426. with transaction.atomic():
  427. # Clear previous stats
  428. PackageBugStats.objects.all().delete()
  429. self._remove_obsolete_action_items(bug_stats.keys())
  430. # Get all packages which have updated stats, along with their
  431. # action items in 2 DB queries.
  432. packages = PackageName.objects.filter(name__in=bug_stats.keys())
  433. packages.prefetch_related('action_items')
  434. # Update stats and action items.
  435. stats = []
  436. for package in packages:
  437. # Save the raw package bug stats
  438. package_bug_stats = PackageBugStats(
  439. package=package, stats=bug_stats[package.name])
  440. stats.append(package_bug_stats)
  441. # Add action items for the package.
  442. self._create_action_items(package_bug_stats)
  443. PackageBugStats.objects.bulk_create(stats)
  444. def update_binary_bugs(self):
  445. """
  446. Performs the update of bug statistics for binary packages.
  447. """
  448. url = 'https://udd.debian.org/cgi-bin/bugs-binpkgs-distro_tracker.cgi'
  449. response_content = get_resource_content(url)
  450. if not response_content:
  451. return
  452. # Extract known binary package bug stats: each line is a separate pkg
  453. bug_stats = {}
  454. for line in response_content.splitlines():
  455. line = line.decode('utf-8')
  456. package_name, bug_counts = line.split(None, 1)
  457. bug_counts = bug_counts.split()
  458. try:
  459. bug_counts = [int(count) for count in bug_counts]
  460. except ValueError:
  461. logger.exception(
  462. 'Failed to parse bug information for {pkg}: {cnts}'.format(
  463. pkg=package_name, cnts=bug_counts))
  464. continue
  465. bug_stats[package_name] = [
  466. {
  467. 'category_name': category_name,
  468. 'bug_count': bug_count,
  469. }
  470. for category_name, bug_count in zip(
  471. self.bug_categories, bug_counts)
  472. ]
  473. with transaction.atomic():
  474. # Clear previous stats
  475. BinaryPackageBugStats.objects.all().delete()
  476. packages = \
  477. BinaryPackageName.objects.filter(name__in=bug_stats.keys())
  478. # Create new stats in a single query
  479. stats = [
  480. BinaryPackageBugStats(package=package,
  481. stats=bug_stats[package.name])
  482. for package in packages
  483. ]
  484. BinaryPackageBugStats.objects.bulk_create(stats)
  485. def execute(self):
  486. # Stats for source and pseudo packages is retrieved from a different
  487. # resource (with a different structure) than stats for binary packages.
  488. self.update_source_and_pseudo_bugs()
  489. self.update_binary_bugs()
  490. class UpdateLintianStatsTask(BaseTask):
  491. """
  492. Updates packages' lintian stats.
  493. """
  494. ACTION_ITEM_TYPE_NAME = 'lintian-warnings-and-errors'
  495. ITEM_DESCRIPTION = 'lintian reports <a href="{url}">{report}</a>'
  496. ITEM_FULL_DESCRIPTION_TEMPLATE = 'debian/lintian-action-item.html'
  497. def __init__(self, force_update=False, *args, **kwargs):
  498. super(UpdateLintianStatsTask, self).__init__(*args, **kwargs)
  499. self.force_update = force_update
  500. self.lintian_action_item_type = ActionItemType.objects.create_or_update(
  501. type_name=self.ACTION_ITEM_TYPE_NAME,
  502. full_description_template=self.ITEM_FULL_DESCRIPTION_TEMPLATE)
  503. def set_parameters(self, parameters):
  504. if 'force_update' in parameters:
  505. self.force_update = parameters['force_update']
  506. def get_lintian_stats(self):
  507. url = 'https://lintian.debian.org/qa-list.txt'
  508. cache = HttpCache(settings.DISTRO_TRACKER_CACHE_DIRECTORY)
  509. response, updated = cache.update(url, force=self.force_update)
  510. response.raise_for_status()
  511. if not updated:
  512. return
  513. all_stats = {}
  514. categories = (
  515. 'errors',
  516. 'warnings',
  517. 'pedantics',
  518. 'experimentals',
  519. 'overriddens',
  520. )
  521. for line in response.iter_lines():
  522. package, stats = line.split(None, 1)
  523. stats = stats.split()
  524. try:
  525. all_stats[package] = {
  526. category: int(count)
  527. for count, category in zip(stats, categories)
  528. }
  529. except ValueError:
  530. logger.exception(
  531. 'Failed to parse lintian information for {pkg}: '
  532. '{line}'.format(
  533. pkg=package, line=line))
  534. continue
  535. return all_stats
  536. def update_action_item(self, package, lintian_stats):
  537. """
  538. Updates the :class:`ActionItem` for the given package based on the
  539. :class:`LintianStats <distro_tracker.vendor.debian.models.LintianStats`
  540. given in ``package_stats``. If the package has errors or warnings an
  541. :class:`ActionItem` is created.
  542. """
  543. package_stats = lintian_stats.stats
  544. warnings, errors = (
  545. package_stats.get('warnings'), package_stats.get('errors', 0))
  546. # Get the old action item for this warning, if it exists.
  547. lintian_action_item = package.get_action_item_for_type(
  548. self.lintian_action_item_type.type_name)
  549. if not warnings and not errors:
  550. if lintian_action_item:
  551. # If the item previously existed, delete it now since there
  552. # are no longer any warnings/errors.
  553. lintian_action_item.delete()
  554. return
  555. # The item didn't previously have an action item: create it now
  556. if lintian_action_item is None:
  557. lintian_action_item = ActionItem(
  558. package=package,
  559. item_type=self.lintian_action_item_type)
  560. lintian_url = lintian_stats.get_lintian_url()
  561. new_extra_data = {
  562. 'warnings': warnings,
  563. 'errors': errors,
  564. 'lintian_url': lintian_url,
  565. }
  566. if lintian_action_item.extra_data:
  567. old_extra_data = lintian_action_item.extra_data
  568. if (old_extra_data['warnings'] == warnings and
  569. old_extra_data['errors'] == errors):
  570. # No need to update
  571. return
  572. lintian_action_item.extra_data = new_extra_data
  573. if errors and warnings:
  574. report = '{} error{} and {} warning{}'.format(
  575. errors,
  576. 's' if errors > 1 else '',
  577. warnings,
  578. 's' if warnings > 1 else '')
  579. elif errors:
  580. report = '{} error{}'.format(
  581. errors,
  582. 's' if errors > 1 else '')
  583. elif warnings:
  584. report = '{} warning{}'.format(
  585. warnings,
  586. 's' if warnings > 1 else '')
  587. lintian_action_item.short_description = self.ITEM_DESCRIPTION.format(
  588. url=lintian_url,
  589. report=report)
  590. # If there are errors make the item a high severity issue
  591. if errors:
  592. lintian_action_item.severity = ActionItem.SEVERITY_HIGH
  593. lintian_action_item.save()
  594. def execute(self):
  595. all_lintian_stats = self.get_lintian_stats()
  596. if not all_lintian_stats:
  597. return
  598. # Discard all old stats
  599. LintianStats.objects.all().delete()
  600. packages = PackageName.objects.filter(name__in=all_lintian_stats.keys())
  601. packages.prefetch_related('action_items')
  602. # Remove action items for packages which no longer have associated
  603. # lintian data.
  604. ActionItem.objects.delete_obsolete_items(
  605. [self.lintian_action_item_type], all_lintian_stats.keys())
  606. stats = []
  607. for package in packages:
  608. package_stats = all_lintian_stats[package.name]
  609. # Save the raw lintian stats.
  610. lintian_stats = LintianStats(package=package, stats=package_stats)
  611. stats.append(lintian_stats)
  612. # Create an ActionItem if there are errors or warnings
  613. self.update_action_item(package, lintian_stats)
  614. LintianStats.objects.bulk_create(stats)
  615. class UpdateTransitionsTask(BaseTask):
  616. REJECT_LIST_URL = 'https://ftp-master.debian.org/transitions.yaml'
  617. PACKAGE_TRANSITION_LIST_URL = (
  618. 'https://release.debian.org/transitions/export/packages.yaml')
  619. def __init__(self, force_update=False, *args, **kwargs):
  620. super(UpdateTransitionsTask, self).__init__(*args, **kwargs)
  621. self.force_update = force_update
  622. self.cache = HttpCache(settings.DISTRO_TRACKER_CACHE_DIRECTORY)
  623. def set_parameters(self, parameters):
  624. if 'force_update' in parameters:
  625. self.force_update = parameters['force_update']
  626. def _get_yaml_resource(self, url):
  627. """
  628. Gets the YAML resource at the given URL and returns it as a Python
  629. object.
  630. """
  631. content = self.cache.get_content(url)
  632. return yaml.safe_load(six.BytesIO(content))
  633. def _add_reject_transitions(self, packages):
  634. """
  635. Adds the transitions which cause uploads to be rejected to the
  636. given ``packages`` dict.
  637. """
  638. reject_list = self._get_yaml_resource(self.REJECT_LIST_URL)
  639. for id, transition in reject_list.items():
  640. for package in transition['packages']:
  641. packages.setdefault(package, {})
  642. packages[package].setdefault(id, {})
  643. packages[package][id]['reject'] = True
  644. packages[package][id]['status'] = 'ongoing'
  645. def _add_package_transition_list(self, packages):
  646. """
  647. Adds the ongoing and planned transitions to the given ``packages``
  648. dict.
  649. """
  650. package_transition_list = self._get_yaml_resource(
  651. self.PACKAGE_TRANSITION_LIST_URL)
  652. wanted_transition_statuses = ('ongoing', 'planned')
  653. for package_info in package_transition_list:
  654. package_name = package_info['name']
  655. for transition_name, status in package_info['list']:
  656. if status not in wanted_transition_statuses:
  657. # Skip transitions with an unwanted status
  658. continue
  659. packages.setdefault(package_name, {})
  660. packages[package_name].setdefault(transition_name, {})
  661. packages[package_name][transition_name]['status'] = status
  662. def execute(self):
  663. # Update the relevant resources first
  664. _, updated_reject_list = self.cache.update(
  665. self.REJECT_LIST_URL, force=self.force_update)
  666. _, updated_package_transition_list = self.cache.update(
  667. self.PACKAGE_TRANSITION_LIST_URL, force=self.force_update)
  668. if not updated_reject_list and not updated_package_transition_list:
  669. # Nothing to do - at least one needs to be updated...
  670. return
  671. package_transitions = {}
  672. self._add_reject_transitions(package_transitions)
  673. self._add_package_transition_list(package_transitions)
  674. PackageTransition.objects.all().delete()
  675. # Get the packages which have transitions
  676. packages = PackageName.objects.filter(
  677. name__in=package_transitions.keys())
  678. transitions = []
  679. for package in packages:
  680. for transition_name, data in \
  681. package_transitions[package.name].items():
  682. transitions.append(PackageTransition(
  683. package=package,
  684. transition_name=transition_name,
  685. status=data.get('status', None),
  686. reject=data.get('reject', False)))
  687. PackageTransition.objects.bulk_create(transitions)
  688. class UpdateExcusesTask(BaseTask):
  689. ACTION_ITEM_TYPE_NAME = 'debian-testing-migration'
  690. ITEM_DESCRIPTION = (
  691. "The package has not entered testing even though the delay is over")
  692. ITEM_FULL_DESCRIPTION_TEMPLATE = 'debian/testing-migration-action-item.html'
  693. def __init__(self, force_update=False, *args, **kwargs):
  694. super(UpdateExcusesTask, self).__init__(*args, **kwargs)
  695. self.force_update = force_update
  696. self.cache = HttpCache(settings.DISTRO_TRACKER_CACHE_DIRECTORY)
  697. self.action_item_type = ActionItemType.objects.create_or_update(
  698. type_name=self.ACTION_ITEM_TYPE_NAME,
  699. full_description_template=self.ITEM_FULL_DESCRIPTION_TEMPLATE)
  700. def set_parameters(self, parameters):
  701. if 'force_update' in parameters:
  702. self.force_update = parameters['force_update']
  703. def _adapt_excuse_links(self, excuse):
  704. """
  705. If the excuse contains any anchor links, convert them to links to Distro
  706. Tracker package pages. Return the original text unmodified, otherwise.
  707. """
  708. re_anchor_href = re.compile(r'^#(.*)$')
  709. html = soup(excuse, 'html.parser')
  710. for a_tag in html.findAll('a', {'href': True}):
  711. href = a_tag['href']
  712. match = re_anchor_href.match(href)
  713. if not match:
  714. continue
  715. package = match.group(1)
  716. a_tag['href'] = reverse('dtracker-package-page', kwargs={
  717. 'package_name': package
  718. })
  719. return str(html)
  720. def _skip_excuses_item(self, item_text):
  721. if not item_text:
  722. return True
  723. # We ignore these excuses
  724. if "Section" in item_text or "Maintainer" in item_text:
  725. return True
  726. return False
  727. def _extract_problems_in_excuses_item(self, subline, package, problematic):
  728. if 'days old (needed' in subline:
  729. words = subline.split()
  730. age, limit = words[0], words[4]
  731. if age != limit:
  732. # It is problematic only when the age is strictly
  733. # greater than the limit.
  734. problematic[package] = {
  735. 'age': age,
  736. 'limit': limit,
  737. }
  738. def _get_excuses_and_problems(self, content_lines):
  739. """
  740. Gets the excuses for each package from the given iterator of lines
  741. representing the excuses html file.
  742. Also finds a list of packages which have not migrated to testing even
  743. after the necessary time has passed.
  744. :returns: A two-tuple where the first element is a dict mapping
  745. package names to a list of excuses. The second element is a dict
  746. mapping package names to a problem information. Problem information
  747. is a dict with the keys ``age`` and ``limit``.
  748. """
  749. try:
  750. # Skip all HTML before the first list
  751. while '<ul>' not in next(content_lines):
  752. pass
  753. except StopIteration:
  754. logger.warning("Invalid format of excuses file")
  755. return
  756. top_level_list = True
  757. package = ""
  758. package_excuses = {}
  759. problematic = {}
  760. excuses = []
  761. for line in content_lines:
  762. if isinstance(line, six.binary_type):
  763. line = line.decode('utf-8')
  764. if '</ul>' in line:
  765. # The inner list is closed -- all excuses for the package are
  766. # processed and we're back to the top-level list.
  767. top_level_list = True
  768. if '/' in package:
  769. continue
  770. # Done with the package
  771. package_excuses[package] = deepcopy(excuses)
  772. continue
  773. if '<ul>' in line:
  774. # Entering the list of excuses
  775. top_level_list = False
  776. continue
  777. if top_level_list:
  778. # The entry in the top level list outside of an inner list is
  779. # a <li> item giving the name of the package for which the
  780. # excuses follow.
  781. words = re.split("[><() ]", line)
  782. package = words[6]
  783. excuses = []
  784. top_level_list = False
  785. continue
  786. line = line.strip()
  787. for subline in line.split("<li>"):
  788. if self._skip_excuses_item(subline):
  789. continue
  790. # Check if there is a problem for the package.
  791. self._extract_problems_in_excuses_item(subline, package,
  792. problematic)
  793. # Extract the rest of the excuses
  794. # If it contains a link to an anchor convert it to a link to a
  795. # package page.
  796. excuses.append(self._adapt_excuse_links(subline))
  797. return package_excuses, problematic
  798. def _create_action_item(self, package, extra_data):
  799. """
  800. Creates a :class:`distro_tracker.core.models.ActionItem` for the given
  801. package including the given extra data. The item indicates that there is
  802. a problem with the package migrating to testing.
  803. """
  804. action_item = \
  805. package.get_action_item_for_type(self.ACTION_ITEM_TYPE_NAME)
  806. if action_item is None:
  807. action_item = ActionItem(
  808. package=package,
  809. item_type=self.action_item_type)
  810. action_item.short_description = self.ITEM_DESCRIPTION
  811. if package.main_entry:
  812. query_string = urlencode({'package': package.name})
  813. extra_data['check_why_url'] = (
  814. 'https://qa.debian.org/excuses.php'
  815. '?{query_string}'.format(query_string=query_string))
  816. action_item.extra_data = extra_data
  817. action_item.save()
  818. def _remove_obsolete_action_items(self, problematic):
  819. """
  820. Remove action items for packages which are no longer problematic.
  821. """
  822. ActionItem.objects.delete_obsolete_items(
  823. item_types=[self.action_item_type],
  824. non_obsolete_packages=problematic.keys())
  825. def _get_update_excuses_content(self):
  826. """
  827. Function returning the content of the update_excuses.html file as an
  828. terable of lines.
  829. Returns ``None`` if the content in the cache is up to date.
  830. """
  831. url = 'https://release.debian.org/britney/update_excuses.html'
  832. response, updated = self.cache.update(url, force=self.force_update)
  833. if not updated:
  834. return
  835. return response.iter_lines(decode_unicode=True)
  836. def execute(self):
  837. content_lines = self._get_update_excuses_content()
  838. if not content_lines:
  839. return
  840. result = self._get_excuses_and_problems(content_lines)
  841. if not result:
  842. return
  843. package_excuses, problematic = result
  844. # Remove stale excuses data and action items which are not still
  845. # problematic.
  846. self._remove_obsolete_action_items(problematic)
  847. PackageExcuses.objects.all().delete()
  848. excuses = []
  849. packages = SourcePackageName.objects.filter(
  850. name__in=package_excuses.keys())
  851. packages.prefetch_related('action_items')
  852. for package in packages:
  853. excuse = PackageExcuses(
  854. package=package,
  855. excuses=package_excuses[package.name])
  856. excuses.append(excuse)
  857. if package.name in problematic:
  858. self._create_action_item(package, problematic[package.name])
  859. # Create all excuses in a single query
  860. PackageExcuses.objects.bulk_create(excuses)
  861. class UpdateBuildLogCheckStats(BaseTask):
  862. ACTION_ITEM_TYPE_NAME = 'debian-build-logcheck'
  863. ITEM_DESCRIPTION = 'Build log checks report <a href="{url}">{report}</a>'
  864. ITEM_FULL_DESCRIPTION_TEMPLATE = 'debian/logcheck-action-item.html'
  865. def __init__(self, force_update=False, *args, **kwargs):
  866. super(UpdateBuildLogCheckStats, self).__init__(*args, **kwargs)
  867. self.force_update = force_update
  868. self.action_item_type = ActionItemType.objects.create_or_update(
  869. type_name=self.ACTION_ITEM_TYPE_NAME,
  870. full_description_template=self.ITEM_FULL_DESCRIPTION_TEMPLATE)
  871. def set_parameters(self, parameters):
  872. if 'force_update' in parameters:
  873. self.force_update = parameters['force_update']
  874. def _get_buildd_content(self):
  875. url = 'https://qa.debian.org/bls/logcheck.txt'
  876. return get_resource_content(url)
  877. def get_buildd_stats(self):
  878. content = self._get_buildd_content()
  879. stats = {}
  880. for line in content.splitlines():
  881. pkg, errors, warnings = line.split("|")[:3]
  882. try:
  883. errors, warnings = int(errors), int(warnings)
  884. except ValueError:
  885. continue
  886. stats[pkg] = {
  887. 'errors': errors,
  888. 'warnings': warnings,
  889. }
  890. return stats
  891. def create_action_item(self, package, stats):
  892. """
  893. Creates a :class:`distro_tracker.core.models.ActionItem` instance for
  894. the given package if the build logcheck stats indicate
  895. """
  896. action_item = \
  897. package.get_action_item_for_type(self.ACTION_ITEM_TYPE_NAME)
  898. errors = stats.get('errors', 0)
  899. warnings = stats.get('warnings', 0)
  900. if not errors and not warnings:
  901. # Remove the previous action item since the package no longer has
  902. # errors/warnings.
  903. if action_item is not None:
  904. action_item.delete()
  905. return
  906. if action_item is None:
  907. action_item = ActionItem(
  908. package=package,
  909. item_type=self.action_item_type)
  910. if action_item.extra_data:
  911. if action_item.extra_data == stats:
  912. # Nothing has changed -- do not update the item
  913. return
  914. logcheck_url = "https://qa.debian.org/bls/packages/{hash}/{pkg}.html"\
  915. .format(hash=package.name[0], pkg=package.name)
  916. if errors and warnings:
  917. report = '{} error{} and {} warning{}'.format(
  918. errors,
  919. 's' if errors > 1 else '',
  920. warnings,
  921. 's' if warnings > 1 else '')
  922. action_item.severity = ActionItem.SEVERITY_HIGH
  923. elif errors:
  924. report = '{} error{}'.format(
  925. errors,
  926. 's' if errors > 1 else '')
  927. action_item.severity = ActionItem.SEVERITY_HIGH
  928. elif warnings:
  929. report = '{} warning{}'.format(
  930. warnings,
  931. 's' if warnings > 1 else '')
  932. action_item.severity = ActionItem.SEVERITY_LOW
  933. action_item.short_description = self.ITEM_DESCRIPTION.format(
  934. url=logcheck_url,
  935. report=report)
  936. action_item.extra_data = stats
  937. action_item.save()
  938. def execute(self):
  939. # Build a dict with stats from both buildd and clang
  940. stats = self.get_buildd_stats()
  941. BuildLogCheckStats.objects.all().delete()
  942. ActionItem.objects.delete_obsolete_items(
  943. [self.action_item_type], stats.keys())
  944. packages = SourcePackageName.objects.filter(name__in=stats.keys())
  945. packages = packages.prefetch_related('action_items')
  946. logcheck_stats = []
  947. for package in packages:
  948. logcheck_stat = BuildLogCheckStats(
  949. package=package,
  950. stats=stats[package.name])
  951. logcheck_stats.append(logcheck_stat)
  952. self.create_action_item(package, stats[package.name])
  953. # One SQL query to create all the stats.
  954. BuildLogCheckStats.objects.bulk_create(logcheck_stats)
  955. class DebianWatchFileScannerUpdate(BaseTask):
  956. ACTION_ITEM_TYPE_NAMES = (
  957. 'new-upstream-version',
  958. 'watch-failure',
  959. )
  960. ACTION_ITEM_TEMPLATES = {
  961. 'new-upstream-version': "debian/new-upstream-version-action-item.html",
  962. 'watch-failure': "debian/watch-failure-action-item.html",
  963. }
  964. ITEM_DESCRIPTIONS = {
  965. 'new-upstream-version': lambda item: (
  966. 'A new upstream version is available: '
  967. '<a href="{url}">{version}</a>'.format(
  968. url=item.extra_data['upstream_url'],
  969. version=item.extra_data['upstream_version'])),
  970. 'watch-failure': lambda item: (
  971. 'Problems while searching for a new upstream version'),
  972. }
  973. ITEM_SEVERITIES = {
  974. 'new-upstream-version': ActionItem.SEVERITY_HIGH,
  975. 'watch-failure': ActionItem.SEVERITY_HIGH,
  976. }
  977. def __init__(self, force_update=False, *args, **kwargs):
  978. super(DebianWatchFileScannerUpdate, self).__init__(*args, **kwargs)
  979. self.force_update = force_update
  980. self.action_item_types = {
  981. type_name: ActionItemType.objects.create_or_update(
  982. type_name=type_name,
  983. full_description_template=self.ACTION_ITEM_TEMPLATES.get(
  984. type_name, None))
  985. for type_name in self.ACTION_ITEM_TYPE_NAMES
  986. }
  987. def set_parameters(self, parameters):
  988. if 'force_update' in parameters:
  989. self.force_update = parameters['force_update']
  990. def _get_upstream_status_content(self):
  991. url = 'https://udd.debian.org/cgi-bin/upstream-status.json.cgi'
  992. return get_resource_content(url)
  993. def _remove_obsolete_action_items(self, item_type_name,
  994. non_obsolete_packages):
  995. """
  996. Removes any existing :class:`ActionItem` with the given type name based
  997. on the list of package names which should still have the items based on
  998. the processed stats.
  999. """
  1000. action_item_type = self.action_item_types[item_type_name]
  1001. ActionItem.objects.delete_obsolete_items(
  1002. item_types=[action_item_type],
  1003. non_obsolete_packages=non_obsolete_packages)
  1004. def get_upstream_status_stats(self, stats):
  1005. """
  1006. Gets the stats from the downloaded data and puts them in the given
  1007. ``stats`` dictionary.
  1008. The keys of the dict are package names.
  1009. :returns: A a two-tuple where the first item is a list of packages
  1010. which have new upstream versions and the second is a list of
  1011. packages which have watch failures.
  1012. """
  1013. content = self._get_upstream_status_content()
  1014. dehs_data = None
  1015. if content:
  1016. dehs_data = json.loads(force_str(content))
  1017. if not dehs_data:
  1018. return [], []
  1019. all_new_versions, all_failures = [], []
  1020. for entry in dehs_data:
  1021. package_name = entry['package']
  1022. if 'status' in entry and ('Newer version' in entry['status'] or
  1023. 'newer package' in entry['status']):
  1024. stats.setdefault(package_name, {})
  1025. stats[package_name]['new-upstream-version'] = {
  1026. 'upstream_version': entry['upstream-version'],
  1027. 'upstream_url': entry['upstream-url'],
  1028. }
  1029. all_new_versions.append(package_name)
  1030. if entry.get('warnings') or entry.get('errors'):
  1031. stats.setdefault(package_name, {})
  1032. msg = '{}\n{}'.format(
  1033. entry.get('errors') or '',
  1034. entry.get('warnings') or '',
  1035. ).strip()
  1036. stats[package_name]['watch-failure'] = {
  1037. 'warning': msg,
  1038. }
  1039. all_failures.append(package_name)
  1040. return all_new_versions, all_failures
  1041. def update_action_item(self, item_type, package, stats):
  1042. """
  1043. Updates the action item of the given type for the given package based
  1044. on the given stats.
  1045. The severity of the item is defined by the :attr:`ITEM_SEVERITIES` dict.
  1046. The short descriptions are created by passing the :class:`ActionItem`
  1047. (with extra data already set) to the callables defined in
  1048. :attr:`ITEM_DESCRIPTIONS`.
  1049. :param item_type: The type of the :class:`ActionItem` that should be
  1050. updated.
  1051. :type item_type: string
  1052. :param package: The package to which this action item should be
  1053. associated.
  1054. :type package: :class:`distro_tracker.core.models.PackageName`
  1055. :param stats: The stats which are used to create the action item.
  1056. :type stats: :class:`dict`
  1057. """
  1058. action_item = package.get_action_item_for_type(item_type)
  1059. if action_item is None:
  1060. # Create an action item...
  1061. action_item = ActionItem(
  1062. package=package,
  1063. item_type=self.action_item_types[item_type])
  1064. if item_type in self.ITEM_SEVERITIES:
  1065. action_item.severity = self.ITEM_SEVERITIES[item_type]
  1066. action_item.extra_data = stats
  1067. action_item.short_description = \
  1068. self.ITEM_DESCRIPTIONS[item_type](action_item)
  1069. action_item.save()
  1070. @transaction.atomic
  1071. def execute(self):
  1072. stats = {}
  1073. new_upstream_version, failures = self.get_upstream_status_stats(stats)
  1074. updated_packages_per_type = {
  1075. 'new-upstream-version': new_upstream_version,
  1076. 'watch-failure': failures,
  1077. }
  1078. # Remove obsolete action items for each of the categories...
  1079. for item_type, packages in updated_packages_per_type.items():
  1080. self._remove_obsolete_action_items(item_type, packages)
  1081. packages = SourcePackageName.objects.filter(
  1082. name__in=stats.keys())
  1083. packages = packages.prefetch_related('action_items')
  1084. # Update action items for each package
  1085. for package in packages:
  1086. for type_name in self.ACTION_ITEM_TYPE_NAMES:
  1087. if type_name in stats[package.name]:
  1088. # method(package, stats[package.name][type_name])
  1089. self.update_action_item(
  1090. type_name, package, stats[package.name][type_name])
  1091. class UpdateSecurityIssuesTask(BaseTask):
  1092. ACTION_ITEM_TYPE_NAME = 'debian-security-issue-in-{}'
  1093. ACTION_ITEM_TEMPLATE = 'debian/security-issue-action-item.html'
  1094. ITEM_DESCRIPTION_TEMPLATE = {
  1095. 'open': '<a href="{url}">{count} security {issue}</a> in {release}',
  1096. 'nodsa':
  1097. '<a href="{url}">{count} ignored security {issue}</a> in {release}',
  1098. 'none': 'No known security issue in {release}',
  1099. }
  1100. def __init__(self, force_update=False, *args, **kwargs):
  1101. super(UpdateSecurityIssuesTask, self).__init__(*args, **kwargs)
  1102. self._action_item_type = {}
  1103. self.force_update = force_update
  1104. def action_item_type(self, release):
  1105. return self._action_item_type.setdefault(
  1106. release, ActionItemType.objects.create_or_update(
  1107. type_name=self.ACTION_ITEM_TYPE_NAME.format(release),
  1108. full_description_template=self.ACTION_ITEM_TEMPLATE))
  1109. def set_parameters(self, parameters):
  1110. if 'force_update' in parameters:
  1111. self.force_update = parameters['force_update']
  1112. def _get_issues_content(self):
  1113. if hasattr(self, '_content'):
  1114. return self._content
  1115. url = 'https://security-tracker.debian.org/tracker/data/json'
  1116. self._content = json.loads(get_resource_content(url))
  1117. return self._content
  1118. @staticmethod
  1119. def get_issues_summary(issues):
  1120. result = {}
  1121. for issue_id, issue_data in six.iteritems(issues):
  1122. for release, data in six.iteritems(issue_data['releases']):
  1123. stats = result.setdefault(release, {
  1124. 'open': 0,
  1125. 'open_details': {},
  1126. 'nodsa': 0,
  1127. 'nodsa_details': {},
  1128. 'unimportant': 0,
  1129. })
  1130. if (data.get('status', '') == 'resolved' or
  1131. data.get('urgency', '') == 'end-of-life'):
  1132. continue
  1133. elif data.get('urgency', '') == 'unimportant':
  1134. stats['unimportant'] += 1
  1135. elif data.get('nodsa', False):
  1136. stats['nodsa'] += 1
  1137. stats['nodsa_details'][issue_id] = \
  1138. issue_data.get('description', '')
  1139. else:
  1140. stats['open'] += 1
  1141. stats['open_details'][issue_id] = \
  1142. issue_data.get('description', '')
  1143. return result
  1144. @classmethod
  1145. def get_issues_stats(cls, content):
  1146. """
  1147. Gets package issue stats from Debian's security tracker.
  1148. """
  1149. stats = {}
  1150. for pkg, issues in six.iteritems(content):
  1151. stats[pkg] = cls.get_issues_summary(issues)
  1152. return stats
  1153. @staticmethod
  1154. def get_data_checksum(data):
  1155. json_dump = json.dumps(data, sort_keys=True)
  1156. if json_dump is not six.binary_type:
  1157. json_dump = json_dump.encode('UTF-8')
  1158. return hashlib.md5(json_dump).hexdigest()
  1159. def _get_short_description(self, key, action_item):
  1160. count = action_item.extra_data['security_issues_count']
  1161. url = 'https://security-tracker.debian.org/tracker/source-package/{}'
  1162. return self.ITEM_DESCRIPTION_TEMPLATE[key].format(
  1163. count=count,
  1164. issue='issues' if count > 1 else 'issue',
  1165. release=action_item.extra_data.get('release', 'sid'),
  1166. url=url.format(action_item.package.name),
  1167. )
  1168. def update_action_item(self, stats, action_item):
  1169. """
  1170. Updates the ``debian-security-issue`` action item based on the count of
  1171. security issues.
  1172. """
  1173. security_issues_count = stats['open'] + stats['nodsa']
  1174. action_item.extra_data['security_issues_count'] = security_issues_count
  1175. action_item.extra_data['open_details'] = stats['open_details']
  1176. action_item.extra_data['nodsa_details'] = stats['nodsa_details']
  1177. if stats['open']:
  1178. action_item.severity = ActionItem.SEVERITY_HIGH
  1179. action_item.short_description = \
  1180. self._get_short_description('open', action_item)
  1181. elif stats['nodsa']:
  1182. action_item.severity = ActionItem.SEVERITY_LOW
  1183. action_item.short_description = \
  1184. self._get_short_description('nodsa', action_item)
  1185. else:
  1186. action_item.severity = ActionItem.SEVERITY_WISHLIST
  1187. action_item.short_description = \
  1188. self._get_short_description('none', action_item)
  1189. @classmethod
  1190. def generate_package_data(cls, issues):
  1191. return {
  1192. 'details': issues,
  1193. 'stats': cls.get_issues_summary(issues),
  1194. 'checksum': cls.get_data_checksum(issues)
  1195. }
  1196. def process_pkg_action_items(self, pkgdata, existing_action_items):
  1197. release_ai = {}
  1198. to_add = []
  1199. to_update = []
  1200. to_drop = []
  1201. global_stats = pkgdata.value.get('stats', {})
  1202. for ai in existing_action_items:
  1203. release = ai.extra_data['release']
  1204. release_ai[release] = ai
  1205. if release not in global_stats:
  1206. to_drop.append(ai)
  1207. for release, stats in global_stats.items():
  1208. count = stats.get('open', 0) + stats.get('nodsa', 0)
  1209. if release in release_ai:
  1210. ai = release_ai[release]
  1211. if count == 0:
  1212. to_drop.append(ai)
  1213. else:
  1214. self.update_action_item(stats, ai)
  1215. to_update.append(ai)
  1216. elif count > 0:
  1217. new_ai = ActionItem(item_type=self.action_item_type(release),
  1218. package=pkgdata.package,
  1219. extra_data={'release': release})
  1220. self.update_action_item(stats, new_ai)
  1221. to_add.append(new_ai)
  1222. return to_add, to_update, to_drop
  1223. def execute(self):
  1224. # Fetch all debian-security PackageExtractedInfo
  1225. all_pkgdata = PackageExtractedInfo.objects.select_related(
  1226. 'package').filter(key='debian-security').only(
  1227. 'package__name', 'value')
  1228. all_data = {}
  1229. packages = {}
  1230. for pkgdata in all_pkgdata:
  1231. all_data[pkgdata.package.name] = pkgdata
  1232. packages[pkgdata.package.name] = pkgdata.package
  1233. # Fetch all debian-security ActionItems
  1234. pkg_action_items = collections.defaultdict(lambda: [])
  1235. all_action_items = ActionItem.objects.select_related(
  1236. 'package').filter(
  1237. item_type__type_name__startswith='debian-security-issue-in-')
  1238. for action_item in all_action_items:
  1239. pkg_action_items[action_item.package.name].append(action_item)
  1240. # Scan the security tracker data
  1241. content = self._get_issues_content()
  1242. to_add = []
  1243. to_update = []
  1244. for pkgname, issues in six.iteritems(content):
  1245. if pkgname in all_data:
  1246. # Check if we need to update the existing data
  1247. checksum = self.get_data_checksum(issues)
  1248. if all_data[pkgname].value.get('checksum', '') == checksum:
  1249. continue
  1250. # Update the data
  1251. pkgdata = all_data[pkgname]
  1252. pkgdata.value = self.generate_package_data(issues)
  1253. to_update.append(pkgdata)
  1254. else:
  1255. # Add data for a new package
  1256. package, _ = PackageName.objects.get_or_create(name=pkgname)
  1257. to_add.append(
  1258. PackageExtractedInfo(
  1259. package=package,
  1260. key='debian-security',
  1261. value=self.generate_package_data(issues)
  1262. )
  1263. )
  1264. # Process action items
  1265. ai_to_add = []
  1266. ai_to_update = []
  1267. ai_to_drop = []
  1268. for pkgdata in itertools.chain(to_add, to_update):
  1269. add, update, drop = self.process_pkg_action_items(
  1270. pkgdata, pkg_action_items[pkgdata.package.name])
  1271. ai_to_add.extend(add)
  1272. ai_to_update.extend(update)
  1273. ai_to_drop.extend(drop)
  1274. # Sync in database
  1275. with transaction.atomic():
  1276. # Delete obsolete data
  1277. PackageExtractedInfo.objects.filter(
  1278. key='debian-security').exclude(
  1279. package__name__in=content.keys()).delete()
  1280. ActionItem.objects.filter(
  1281. item_type__type_name__startswith='debian-security-issue-in-'
  1282. ).exclude(package__name__in=content.keys()).delete()
  1283. ActionItem.objects.filter(
  1284. item_type__type_name__startswith='debian-security-issue-in-',
  1285. id__in=[ai.id for ai in ai_to_drop]).delete()
  1286. # Add new entries
  1287. PackageExtractedInfo.objects.bulk_create(to_add)
  1288. ActionItem.objects.bulk_create(ai_to_add)
  1289. # Update existing entries
  1290. for pkgdata in to_update:
  1291. pkgdata.save()
  1292. for ai in ai_to_update:
  1293. ai.save()
  1294. class UpdatePiuPartsTask(BaseTask):
  1295. """
  1296. Retrieves the piuparts stats for all the suites defined in the
  1297. :data:`distro_tracker.project.local_settings.DISTRO_TRACKER_DEBIAN_PIUPARTS_SUITES`
  1298. """
  1299. ACTION_ITEM_TYPE_NAME = 'debian-piuparts-test-fail'
  1300. ACTION_ITEM_TEMPLATE = 'debian/piuparts-action-item.html'
  1301. ITEM_DESCRIPTION = 'piuparts found (un)installation error(s)'
  1302. def __init__(self, force_update=False, *args, **kwargs):
  1303. super(UpdatePiuPartsTask, self).__init__(*args, **kwargs)
  1304. self.force_update = force_update
  1305. self.action_item_type = ActionItemType.objects.create_or_update(
  1306. type_name=self.ACTION_ITEM_TYPE_NAME,
  1307. full_description_template=self.ACTION_ITEM_TEMPLATE)
  1308. def set_parameters(self, parameters):
  1309. if 'force_update' in parameters:
  1310. self.force_update = parameters['force_update']
  1311. def _get_piuparts_content(self, suite):
  1312. """
  1313. :returns: The content of the piuparts report for the given package
  1314. or ``None`` if there is no data for the particular suite.
  1315. """
  1316. url = 'https://piuparts.debian.org/{suite}/sources.txt'
  1317. return get_resource_content(url.format(suite=suite))
  1318. def get_piuparts_stats(self):
  1319. suites = getattr(settings, 'DISTRO_TRACKER_DEBIAN_PIUPARTS_SUITES', [])
  1320. failing_packages = {}
  1321. for suite in suites:
  1322. content = self._get_piuparts_content(suite)
  1323. if content is None:
  1324. logger.info("There is no piuparts for suite: {}".format(suite))
  1325. continue
  1326. for line in content.splitlines():
  1327. package_name, status = line.split(':', 1)
  1328. package_name, status = package_name.strip(), status.strip()
  1329. if status == 'fail':
  1330. failing_packages.setdefault(package_name, [])
  1331. failing_packages[package_name].append(suite)
  1332. return failing_packages
  1333. def create_action_item(self, package, suites):
  1334. """
  1335. Creates an :class:`ActionItem <distro_tracker.core.models.ActionItem>`
  1336. instance for the package based on the list of suites in which the
  1337. piuparts installation test failed.
  1338. """
  1339. action_item = package.get_action_item_for_type(self.action_item_type)
  1340. if action_item is None:
  1341. action_item = ActionItem(
  1342. package=package,
  1343. item_type=self.action_item_type,
  1344. short_description=self.ITEM_DESCRIPTION)
  1345. if action_item.extra_data:
  1346. existing_items = action_item.extra_data.get('suites', [])
  1347. if list(sorted(existing_items)) == list(sorted(suites)):
  1348. # No need to update this item
  1349. return
  1350. action_item.extra_data = {
  1351. 'suites': suites,
  1352. }
  1353. action_item.save()
  1354. def execute(self):
  1355. failing_packages = self.get_piuparts_stats()
  1356. ActionItem.objects.delete_obsolete_items(
  1357. item_types=[self.action_item_type],
  1358. non_obsolete_packages=failing_packages.keys())
  1359. packages = SourcePackageName.objects.filter(
  1360. name__in=failing_packages.keys())
  1361. packages = packages.prefetch_related('action_items')
  1362. for package in packages:
  1363. self.create_action_item(package, failing_packages[package.name])
  1364. class UpdateUbuntuStatsTask(BaseTask):
  1365. """
  1366. The task updates Ubuntu stats for packages. These stats are displayed in a
  1367. separate panel.
  1368. """
  1369. def __init__(self, force_update=False, *args, **kwargs):
  1370. super(UpdateUbuntuStatsTask, self).__init__(*args, **kwargs)
  1371. self.force_update = force_update
  1372. self.cache = HttpCache(settings.DISTRO_TRACKER_CACHE_DIRECTORY)
  1373. def set_parameters(self, parameters):
  1374. if 'force_update' in parameters:
  1375. self.force_update = parameters['force_update']
  1376. def _get_versions_content(self):
  1377. url = 'https://udd.debian.org/cgi-bin/ubuntupackages.cgi'
  1378. return get_resource_content(url)
  1379. def get_ubuntu_versions(self):
  1380. """
  1381. Retrieves the Ubuntu package versions.
  1382. :returns: A dict mapping package names to Ubuntu versions.
  1383. """
  1384. content = self._get_versions_content()
  1385. package_versions = {}
  1386. for line in content.splitlines():
  1387. package, version = line.split(' ', 1)
  1388. version = version.strip()
  1389. package_versions[package] = version
  1390. return package_versions
  1391. def _get_bug_stats_content(self):
  1392. url = 'https://udd.debian.org/cgi-bin/ubuntubugs.cgi'
  1393. return get_resource_content(url)
  1394. def get_ubuntu_bug_stats(self):
  1395. """
  1396. Retrieves the Ubuntu bug stats of a package. Bug stats contain the
  1397. count of bugs and the count of patches.
  1398. :returns: A dict mapping package names to a dict of package stats.
  1399. """
  1400. content = self._get_bug_stats_content()
  1401. bug_stats = {}
  1402. for line in content.splitlines():
  1403. package_name, bug_count, patch_count = line.split("|", 2)
  1404. try:
  1405. bug_count, patch_count = int(bug_count), int(patch_count)
  1406. except ValueError:
  1407. continue
  1408. bug_stats[package_name] = {
  1409. 'bug_count': bug_count,
  1410. 'patch_count': patch_count,
  1411. }
  1412. return bug_stats
  1413. def _get_ubuntu_patch_diff_content(self):
  1414. url = 'https://patches.ubuntu.com/PATCHES'
  1415. return get_resource_content(url)
  1416. def get_ubuntu_patch_diffs(self):
  1417. """
  1418. Retrieves the Ubuntu patch diff information. The information consists
  1419. of the diff URL and the version of the Ubuntu package to which the
  1420. diff belongs to.
  1421. :returns: A dict mapping package names to diff information.
  1422. """
  1423. content = self._get_ubuntu_patch_diff_content()
  1424. patch_diffs = {}
  1425. re_diff_version = re.compile(r'_(\S+)\.patch')
  1426. for line in content.splitlines():
  1427. package_name, diff_url = line.split(' ', 1)
  1428. # Extract the version of the package from the diff url
  1429. match = re_diff_version.search(diff_url)
  1430. if not match:
  1431. # Invalid URL: no version
  1432. continue
  1433. version = match.group(1)
  1434. patch_diffs[package_name] = {
  1435. 'version': version,
  1436. 'diff_url': diff_url
  1437. }
  1438. return patch_diffs
  1439. def execute(self):
  1440. package_versions = self.get_ubuntu_versions()
  1441. bug_stats = self.get_ubuntu_bug_stats()
  1442. patch_diffs = self.get_ubuntu_patch_diffs()
  1443. obsolete_ubuntu_pkgs = UbuntuPackage.objects.exclude(
  1444. package__name__in=package_versions.keys())
  1445. obsolete_ubuntu_pkgs.delete()
  1446. packages = PackageName.objects.filter(name__in=package_versions.keys())
  1447. packages = packages.prefetch_related('ubuntu_package')
  1448. for package in packages:
  1449. version = package_versions[package.name]
  1450. bugs = bug_stats.get(package.name, None)
  1451. diff = patch_diffs.get(package.name, None)
  1452. try:
  1453. ubuntu_package = package.ubuntu_package
  1454. ubuntu_package.version = version
  1455. ubuntu_package.bugs = bugs
  1456. ubuntu_package.patch_diff = diff
  1457. ubuntu_package.save()
  1458. except UbuntuPackage.DoesNotExist:
  1459. ubuntu_package = UbuntuPackage.objects.create(
  1460. package=package,
  1461. version=version,
  1462. bugs=bugs,
  1463. patch_diff=diff)
  1464. class UpdateDebianDuckTask(BaseTask):
  1465. """
  1466. A task for updating upstream url issue information on all packages.
  1467. """
  1468. DUCK_LINK = 'http://duck.debian.net'
  1469. # URL of the list of source packages with issues.
  1470. DUCK_SP_LIST_URL = 'http://duck.debian.net/static/sourcepackages.txt'
  1471. ACTION_ITEM_TYPE_NAME = 'debian-duck'
  1472. ACTION_ITEM_TEMPLATE = 'debian/duck-action-item.html'
  1473. ITEM_DESCRIPTION = 'The URL(s) for this package had some ' + \
  1474. 'recent persistent <a href="{issues_link}">issues</a>'
  1475. def __init__(self, force_update=False, *args, **kwargs):
  1476. super(UpdateDebianDuckTask, self).__init__(*args, **kwargs)
  1477. self.force_update = force_update
  1478. self.action_item_type = ActionItemType.objects.create_or_update(
  1479. type_name=self.ACTION_ITEM_TYPE_NAME,
  1480. full_description_template=self.ACTION_ITEM_TEMPLATE)
  1481. def set_parameters(self, parameters):
  1482. if 'force_update' in parameters:
  1483. self.force_update = parameters['force_update']
  1484. def _get_duck_urls_content(self):
  1485. """
  1486. Gets the list of packages with URL issues from
  1487. duck.debian.net
  1488. :returns: A array if source package names.
  1489. """
  1490. ducklist = get_resource_content(self.DUCK_SP_LIST_URL)
  1491. if ducklist is None:
  1492. return None
  1493. packages = []
  1494. for package_name in ducklist.splitlines():
  1495. package_name = package_name.strip()
  1496. packages.append(package_name)
  1497. return packages
  1498. def update_action_item(self, package):
  1499. action_item = package.get_action_item_for_type(self.action_item_type)
  1500. if not action_item:
  1501. action_item = ActionItem(
  1502. package=package,
  1503. item_type=self.action_item_type,
  1504. )
  1505. issues_link = self.DUCK_LINK + "/static/sp/" \
  1506. + package_hashdir(package.name) + "/" + package.name + ".html"
  1507. action_item.short_description = \
  1508. self.ITEM_DESCRIPTION.format(issues_link=issues_link)
  1509. action_item.extra_data = {
  1510. 'duck_link': self.DUCK_LINK,
  1511. 'issues_link': issues_link
  1512. }
  1513. action_item.severity = ActionItem.SEVERITY_LOW
  1514. action_item.save()
  1515. def execute(self):
  1516. ducklings = self._get_duck_urls_content()
  1517. if ducklings is None:
  1518. return
  1519. ActionItem.objects.delete_obsolete_items(
  1520. item_types=[self.action_item_type],
  1521. non_obsolete_packages=ducklings)
  1522. packages = SourcePackageName.objects.filter(name__in=ducklings)
  1523. for package in packages:
  1524. self.update_action_item(package)
  1525. class UpdateWnppStatsTask(BaseTask):
  1526. """
  1527. The task updates the WNPP bugs for all packages.
  1528. """
  1529. ACTION_ITEM_TYPE_NAME = 'debian-wnpp-issue'
  1530. ACTION_ITEM_TEMPLATE = 'debian/wnpp-action-item.html'
  1531. ITEM_DESCRIPTION = '<a href="{url}">{wnpp_type}: {wnpp_msg}</a>'
  1532. def __init__(self, force_update=False, *args, **kwargs):
  1533. super(UpdateWnppStatsTask, self).__init__(*args, **kwargs)
  1534. self.force_update = force_update
  1535. self.action_item_type = ActionItemType.objects.create_or_update(
  1536. type_name=self.ACTION_ITEM_TYPE_NAME,
  1537. full_description_template=self.ACTION_ITEM_TEMPLATE)
  1538. def set_parameters(self, parameters):
  1539. if 'force_update' in parameters:
  1540. self.force_update = parameters['force_update']
  1541. def _get_wnpp_content(self):
  1542. url = 'https://qa.debian.org/data/bts/wnpp_rm'
  1543. cache = HttpCache(settings.DISTRO_TRACKER_CACHE_DIRECTORY)
  1544. if not cache.is_expired(url):
  1545. return
  1546. response, updated = cache.update(url, force=self.force_update)
  1547. if not updated:
  1548. return
  1549. return response.content
  1550. def get_wnpp_stats(self):
  1551. """
  1552. Retrieves and parses the wnpp stats for all packages. WNPP stats
  1553. include the WNPP type and the BTS bug id.
  1554. :returns: A dict mapping package names to wnpp stats.
  1555. """
  1556. content = self._get_wnpp_content()
  1557. if content is None:
  1558. return
  1559. wnpp_stats = {}
  1560. for line in content.splitlines():
  1561. line = line.strip()
  1562. try:
  1563. package_name, wnpp_type, bug_id = line.split('|')[0].split()
  1564. bug_id = int(bug_id)
  1565. except:
  1566. # Badly formatted
  1567. continue
  1568. # Strip the colon from the end of the package name
  1569. package_name = package_name[:-1]
  1570. wnpp_stats[package_name] = {
  1571. 'wnpp_type': wnpp_type,
  1572. 'bug_id': bug_id,
  1573. }
  1574. return wnpp_stats
  1575. def update_action_item(self, package, stats):
  1576. """
  1577. Creates an :class:`ActionItem <distro_tracker.core.models.ActionItem>`
  1578. instance for the given type indicating that the package has a WNPP
  1579. issue.
  1580. """
  1581. action_item = package.get_action_item_for_type(self.action_item_type)
  1582. if not action_item:
  1583. action_item = ActionItem(
  1584. package=package,
  1585. item_type=self.action_item_type)
  1586. # Check if the stats have actually been changed
  1587. if action_item.extra_data:
  1588. if action_item.extra_data.get('wnpp_info', None) == stats:
  1589. # Nothing to do -- stll the same data
  1590. return
  1591. # Update the data since something has changed
  1592. try:
  1593. release = package.main_entry.repository.suite or \
  1594. package.main_entry.repository.codename
  1595. except:
  1596. release = None
  1597. msgs = {
  1598. 'O': "This package has been orphaned and needs a maintainer.",
  1599. 'ITA': "Someone intends to adopt this package.",
  1600. 'RFA': "The maintainer is looking for someone adopt this package.",
  1601. 'RFH': "The maintainer is looking for help with this package.",
  1602. 'ITP': "Someone is planning to reintroduce this package.",
  1603. 'RFP': "There is a request to reintroduced this package.",
  1604. 'RM': "This package has been requested to be removed.",
  1605. '?': "The WNPP database contains an entry for this package."
  1606. }
  1607. wnpp_type = stats['wnpp_type']
  1608. try:
  1609. wnpp_msg = msgs[wnpp_type]
  1610. except KeyError:
  1611. wnpp_msg = msgs['?']
  1612. action_item.short_description = self.ITEM_DESCRIPTION.format(
  1613. url='https://bugs.debian.org/{}'.format(stats['bug_id']),
  1614. wnpp_type=wnpp_type, wnpp_msg=wnpp_msg)
  1615. action_item.extra_data = {
  1616. 'wnpp_info': stats,
  1617. 'release': release,
  1618. }
  1619. action_item.save()
  1620. def update_depneedsmaint_action_item(self, package_needs_maintainer, stats):
  1621. short_description_template = \
  1622. 'Depends on packages which need a new maintainer'
  1623. package_needs_maintainer.get_absolute_url()
  1624. action_item_type = ActionItemType.objects.create_or_update(
  1625. type_name='debian-depneedsmaint',
  1626. full_description_template='debian/depneedsmaint-action-item.html')
  1627. dependencies = SourcePackageDeps.objects.filter(
  1628. dependency=package_needs_maintainer)
  1629. for dependency in dependencies:
  1630. package = dependency.source
  1631. action_item = package.get_action_item_for_type(action_item_type)
  1632. if not action_item:
  1633. action_item = ActionItem(
  1634. package=package,
  1635. item_type=action_item_type,
  1636. extra_data={})
  1637. pkgdata = {
  1638. 'bug': stats['bug_id'],
  1639. 'details': dependency.details,
  1640. }
  1641. if (action_item.extra_data.get(package_needs_maintainer.name, {}) ==
  1642. pkgdata):
  1643. # Nothing has changed
  1644. continue
  1645. action_item.short_description = short_description_template
  1646. action_item.extra_data[package_needs_maintainer.name] = pkgdata
  1647. action_item.save()
  1648. @transaction.atomic
  1649. def execute(self):
  1650. wnpp_stats = self.get_wnpp_stats()
  1651. if wnpp_stats is None:
  1652. # Nothing to do: cached content up to date
  1653. return
  1654. ActionItem.objects.delete_obsolete_items(
  1655. item_types=[self.action_item_type],
  1656. non_obsolete_packages=wnpp_stats.keys())
  1657. # Remove obsolete action items for packages whose dependencies need a
  1658. # new maintainer.
  1659. packages_need_maintainer = []
  1660. for name, stats in wnpp_stats.items():
  1661. if stats['wnpp_type'] in ('O', 'RFA'):
  1662. packages_need_maintainer.append(name)
  1663. packages_depneeds_maint = [
  1664. package.name for package in SourcePackageName.objects.filter(
  1665. source_dependencies__dependency__name__in=packages_need_maintainer) # noqa
  1666. ]
  1667. ActionItem.objects.delete_obsolete_items(
  1668. item_types=[
  1669. ActionItemType.objects.get_or_create(
  1670. type_name='debian-depneedsmaint')[0],
  1671. ],
  1672. non_obsolete_packages=packages_depneeds_maint)
  1673. # Drop all reverse references
  1674. for ai in ActionItem.objects.filter(
  1675. item_type__type_name='debian-depneedsmaint'):
  1676. ai.extra_data = {}
  1677. ai.save()
  1678. packages = SourcePackageName.objects.filter(name__in=wnpp_stats.keys())
  1679. packages = packages.prefetch_related('action_items')
  1680. for package in packages:
  1681. stats = wnpp_stats[package.name]
  1682. self.update_action_item(package, stats)
  1683. # Update action items for packages which depend on this one to
  1684. # indicate that a dependency needs a new maintainer.
  1685. if package.name in packages_need_maintainer:
  1686. self.update_depneedsmaint_action_item(package, stats)
  1687. class UpdateNewQueuePackages(BaseTask):
  1688. """
  1689. Updates the versions of source packages found in the NEW queue.
  1690. """
  1691. EXTRACTED_INFO_KEY = 'debian-new-queue-info'
  1692. def __init__(self, force_update=False, *args, **kwargs):
  1693. super(UpdateNewQueuePackages, self).__init__(*args, **kwargs)
  1694. self.force_update = force_update
  1695. def set_parameters(self, parameters):
  1696. if 'force_update' in parameters:
  1697. self.force_update = parameters['force_update']
  1698. def _get_new_content(self):
  1699. """
  1700. :returns: The content of the deb822 formatted file giving the list of
  1701. packages found in NEW.
  1702. ``None`` if the cached resource is up to date.
  1703. """
  1704. url = 'https://ftp-master.debian.org/new.822'
  1705. cache = HttpCache(settings.DISTRO_TRACKER_CACHE_DIRECTORY)
  1706. if not cache.is_expired(url):
  1707. return
  1708. response, updated = cache.update(url, force=self.force_update)
  1709. if not updated:
  1710. return
  1711. return response.content
  1712. def extract_package_info(self, content):
  1713. """
  1714. Extracts the package information from the content of the NEW queue.
  1715. :returns: A dict mapping package names to a dict mapping the
  1716. distribution name in which the package is found to the version
  1717. information for the most recent version of the package in the dist.
  1718. """
  1719. packages = {}
  1720. for stanza in deb822.Deb822.iter_paragraphs(content.splitlines()):
  1721. necessary_fields = ('Source', 'Queue', 'Version', 'Distribution')
  1722. if not all(field in stanza for field in necessary_fields):
  1723. continue
  1724. if stanza['Queue'] != 'new':
  1725. continue
  1726. versions = stanza['Version'].split()
  1727. # Save only the most recent version
  1728. version = max(versions, key=lambda x: AptPkgVersion(x))
  1729. package_name = stanza['Source']
  1730. pkginfo = packages.setdefault(package_name, {})
  1731. distribution = stanza['Distribution']
  1732. if distribution in pkginfo:
  1733. current_version = pkginfo[distribution]['version']
  1734. if debian_support.version_compare(version, current_version) < 0:
  1735. # The already saved version is more recent than this one.
  1736. continue
  1737. pkginfo[distribution] = {
  1738. 'version': version,
  1739. }
  1740. return packages
  1741. def execute(self):
  1742. content = self._get_new_content()
  1743. all_package_info = self.extract_package_info(content)
  1744. packages = SourcePackageName.objects.filter(
  1745. name__in=all_package_info.keys())
  1746. with transaction.atomic():
  1747. # Drop old entries
  1748. PackageExtractedInfo.objects.filter(
  1749. key=self.EXTRACTED_INFO_KEY).delete()
  1750. # Prepare current entries
  1751. extracted_info = []
  1752. for package in packages:
  1753. new_queue_info = PackageExtractedInfo(
  1754. key=self.EXTRACTED_INFO_KEY,
  1755. package=package,
  1756. value=all_package_info[package.name])
  1757. extracted_info.append(new_queue_info)
  1758. # Bulk create them
  1759. PackageExtractedInfo.objects.bulk_create(extracted_info)
  1760. class UpdateDebciStatusTask(BaseTask):
  1761. """
  1762. Updates packages' debci status.
  1763. """
  1764. ACTION_ITEM_TYPE_NAME = 'debci-failed-tests'
  1765. ITEM_DESCRIPTION = (
  1766. 'Debci reports <a href="{debci_url}">failed tests</a> '
  1767. '(<a href="{log_url}">log</a>)'
  1768. )
  1769. ITEM_FULL_DESCRIPTION_TEMPLATE = 'debian/debci-action-item.html'
  1770. def __init__(self, force_update=False, *args, **kwargs):
  1771. super(UpdateDebciStatusTask, self).__init__(*args, **kwargs)
  1772. self.force_update = force_update
  1773. self.debci_action_item_type = ActionItemType.objects.create_or_update(
  1774. type_name=self.ACTION_ITEM_TYPE_NAME,
  1775. full_description_template=self.ITEM_FULL_DESCRIPTION_TEMPLATE)
  1776. def set_parameters(self, parameters):
  1777. if 'force_update' in parameters:
  1778. self.force_update = parameters['force_update']
  1779. def get_debci_status(self):
  1780. url = 'https://ci.debian.net/data/status/unstable/amd64/packages.json'
  1781. cache = HttpCache(settings.DISTRO_TRACKER_CACHE_DIRECTORY)
  1782. response, updated = cache.update(url, force=self.force_update)
  1783. response.raise_for_status()
  1784. if not updated:
  1785. return
  1786. debci_status = json.loads(response.text)
  1787. return debci_status
  1788. def update_action_item(self, package, debci_status):
  1789. """
  1790. Updates the :class:`ActionItem` for the given package based on the
  1791. :class:`DebciStatus <distro_tracker.vendor.debian.models.DebciStatus`
  1792. If the package has test failures an :class:`ActionItem` is created.
  1793. """
  1794. debci_action_item = package.get_action_item_for_type(
  1795. self.debci_action_item_type.type_name)
  1796. if debci_status.get('status') == 'pass':
  1797. debci_action_item.delete()
  1798. return
  1799. if debci_action_item is None:
  1800. debci_action_item = ActionItem(
  1801. package=package,
  1802. item_type=self.debci_action_item_type,
  1803. severity=ActionItem.SEVERITY_HIGH)
  1804. package_name = debci_status.get('package')
  1805. if package_name[:3] == 'lib':
  1806. log_dir = package_name[:4]
  1807. else:
  1808. log_dir = package_name[:1]
  1809. url = 'https://ci.debian.net/packages/' + log_dir + '/' + \
  1810. package_name + '/'
  1811. log = 'https://ci.debian.net/data/packages/unstable/amd64/' + \
  1812. log_dir + "/" + package_name + '/latest-autopkgtest/log.gz'
  1813. debci_action_item.short_description = self.ITEM_DESCRIPTION.format(
  1814. debci_url=url,
  1815. log_url=log)
  1816. debci_action_item.extra_data = {
  1817. 'duration': debci_status.get('duration_human'),
  1818. 'previous_status': debci_status.get('previous_status'),
  1819. 'date': debci_status.get('date'),
  1820. 'url': url,
  1821. 'log': log,
  1822. }
  1823. debci_action_item.save()
  1824. def execute(self):
  1825. all_debci_status = self.get_debci_status()
  1826. if all_debci_status is None:
  1827. return
  1828. with transaction.atomic():
  1829. packages = []
  1830. for result in all_debci_status:
  1831. if result['status'] == 'fail':
  1832. try:
  1833. package = SourcePackageName.objects.get(
  1834. name=result['package'])
  1835. packages.append(package)
  1836. self.update_action_item(package, result)
  1837. except SourcePackageName.DoesNotExist:
  1838. pass
  1839. # Remove action items for packages without failing tests.
  1840. ActionItem.objects.delete_obsolete_items(
  1841. [self.debci_action_item_type], packages)
  1842. class UpdateAutoRemovalsStatsTask(BaseTask):
  1843. """
  1844. A task for updating autoremovals information on all packages.
  1845. """
  1846. ACTION_ITEM_TYPE_NAME = 'debian-autoremoval'
  1847. ACTION_ITEM_TEMPLATE = 'debian/autoremoval-action-item.html'
  1848. ITEM_DESCRIPTION = 'Marked for autoremoval on {removal_date}: {bugs}'
  1849. def __init__(self, force_update=False, *args, **kwargs):
  1850. super(UpdateAutoRemovalsStatsTask, self).__init__(*args, **kwargs)
  1851. self.force_update = force_update
  1852. self.action_item_type = ActionItemType.objects.create_or_update(
  1853. type_name=self.ACTION_ITEM_TYPE_NAME,
  1854. full_description_template=self.ACTION_ITEM_TEMPLATE)
  1855. def set_parameters(self, parameters):
  1856. if 'force_update' in parameters:
  1857. self.force_update = parameters['force_update']
  1858. def get_autoremovals_stats(self):
  1859. """
  1860. Retrieves and parses the autoremoval stats for all packages.
  1861. Autoremoval stats include the BTS bugs id.
  1862. :returns: A dict mapping package names to autoremoval stats.
  1863. """
  1864. content = get_resource_content(
  1865. 'https://udd.debian.org/cgi-bin/autoremovals.yaml.cgi')
  1866. if content:
  1867. return yaml.safe_load(six.BytesIO(content))
  1868. def update_action_item(self, package, stats):
  1869. """
  1870. Creates an :class:`ActionItem <distro_tracker.core.models.ActionItem>`
  1871. instance for the given type indicating that the package has an
  1872. autoremoval issue.
  1873. """
  1874. action_item = package.get_action_item_for_type(self.action_item_type)
  1875. if not action_item:
  1876. action_item = ActionItem(
  1877. package=package,
  1878. item_type=self.action_item_type,
  1879. severity=ActionItem.SEVERITY_HIGH)
  1880. bugs_dependencies = stats.get('bugs_dependencies', [])
  1881. buggy_dependencies = stats.get('buggy_dependencies', [])
  1882. all_bugs = stats['bugs'] + bugs_dependencies
  1883. link = '<a href="https://bugs.debian.org/{}">{}</a>'
  1884. removal_date = stats['removal_date'].strftime('%d %B')
  1885. if removal_date is six.binary_type:
  1886. removal_date = removal_date.decode('utf-8', 'ignore')
  1887. action_item.short_description = self.ITEM_DESCRIPTION.format(
  1888. removal_date=removal_date,
  1889. bugs=', '.join(link.format(bug, bug) for bug in all_bugs))
  1890. if hasattr(stats['removal_date'], 'strftime'):
  1891. stats['removal_date'] = stats['removal_date'].strftime(
  1892. '%a %d %b %Y')
  1893. action_item.extra_data = {
  1894. 'stats': stats,
  1895. 'removal_date': stats['removal_date'],
  1896. 'bugs': ', '.join(link.format(bug, bug) for bug in stats['bugs']),
  1897. 'bugs_dependencies': ', '.join(
  1898. link.format(bug, bug) for bug in bugs_dependencies),
  1899. 'buggy_dependencies': ' and '.join(
  1900. ['<a href="{}">{}</a>'.format(
  1901. reverse(
  1902. 'dtracker-package-page',
  1903. kwargs={'package_name': p}),
  1904. p) for p in buggy_dependencies])}
  1905. action_item.save()
  1906. def execute(self):
  1907. autoremovals_stats = self.get_autoremovals_stats()
  1908. if autoremovals_stats is None:
  1909. # Nothing to do: cached content up to date
  1910. return
  1911. ActionItem.objects.delete_obsolete_items(
  1912. item_types=[self.action_item_type],
  1913. non_obsolete_packages=autoremovals_stats.keys())
  1914. packages = SourcePackageName.objects.filter(
  1915. name__in=autoremovals_stats.keys())
  1916. packages = packages.prefetch_related('action_items')
  1917. for package in packages:
  1918. self.update_action_item(package, autoremovals_stats[package.name])
  1919. class UpdatePackageScreenshotsTask(BaseTask):
  1920. """
  1921. Check if a screenshot exists on screenshots.debian.net, and add a
  1922. key to PackageExtractedInfo if it does.
  1923. """
  1924. EXTRACTED_INFO_KEY = 'screenshots'
  1925. def __init__(self, force_update=False, *args, **kwargs):
  1926. super(UpdatePackageScreenshotsTask, self).__init__(*args, **kwargs)
  1927. self.force_update = force_update
  1928. def set_parameters(self, parameters):
  1929. if 'force_update' in parameters:
  1930. self.force_update = parameters['force_update']
  1931. def _get_screenshots(self):
  1932. url = 'https://screenshots.debian.net/json/packages'
  1933. cache = HttpCache(settings.DISTRO_TRACKER_CACHE_DIRECTORY)
  1934. response, updated = cache.update(url, force=self.force_update)
  1935. response.raise_for_status()
  1936. if not updated:
  1937. return
  1938. data = json.loads(response.text)
  1939. return data
  1940. def execute(self):
  1941. content = self._get_screenshots()
  1942. if content is None:
  1943. return
  1944. packages_with_screenshots = []
  1945. for item in content['packages']:
  1946. try:
  1947. package = SourcePackageName.objects.get(name=item['name'])
  1948. packages_with_screenshots.append(package)
  1949. except SourcePackageName.DoesNotExist:
  1950. pass
  1951. with transaction.atomic():
  1952. PackageExtractedInfo.objects.filter(key='screenshots').delete()
  1953. extracted_info = []
  1954. for package in packages_with_screenshots:
  1955. try:
  1956. screenshot_info = package.packageextractedinfo_set.get(
  1957. key=self.EXTRACTED_INFO_KEY)
  1958. screenshot_info.value['screenshots'] = 'true'
  1959. except PackageExtractedInfo.DoesNotExist:
  1960. screenshot_info = PackageExtractedInfo(
  1961. key=self.EXTRACTED_INFO_KEY,
  1962. package=package,
  1963. value={'screenshots': 'true'})
  1964. extracted_info.append(screenshot_info)
  1965. PackageExtractedInfo.objects.bulk_create(extracted_info)
  1966. class UpdateBuildReproducibilityTask(BaseTask):
  1967. BASE_URL = 'https://tests.reproducible-builds.org'
  1968. ACTION_ITEM_TYPE_NAME = 'debian-build-reproducibility'
  1969. ACTION_ITEM_TEMPLATE = 'debian/build-reproducibility-action-item.html'
  1970. ITEM_DESCRIPTION = {
  1971. 'blacklisted': '<a href="{url}">Blacklisted</a> from build '
  1972. 'reproducibility testing',
  1973. 'FTBFS': '<a href="{url}">Fails to build</a> during reproducibility '
  1974. 'testing',
  1975. 'reproducible': None,
  1976. 'unreproducible': '<a href="{url}">Does not build reproducibly</a> '
  1977. 'during testing',
  1978. '404': None,
  1979. 'not for us': None,
  1980. }
  1981. def __init__(self, force_update=False, *args, **kwargs):
  1982. super(UpdateBuildReproducibilityTask, self).__init__(*args, **kwargs)
  1983. self.force_update = force_update
  1984. self.action_item_type = ActionItemType.objects.create_or_update(
  1985. type_name=self.ACTION_ITEM_TYPE_NAME,
  1986. full_description_template=self.ACTION_ITEM_TEMPLATE)
  1987. def set_parameters(self, parameters):
  1988. if 'force_update' in parameters:
  1989. self.force_update = parameters['force_update']
  1990. def get_build_reproducibility(self):
  1991. url = '{}/debian/reproducible-tracker.json'.format(self.BASE_URL)
  1992. cache = HttpCache(settings.DISTRO_TRACKER_CACHE_DIRECTORY)
  1993. if not self.force_update and not cache.is_expired(url):
  1994. return
  1995. response, updated = cache.update(url, force=self.force_update)
  1996. response.raise_for_status()
  1997. if not updated:
  1998. return
  1999. reproducibilities = json.loads(response.text)
  2000. packages = {}
  2001. for item in reproducibilities:
  2002. package = item['package']
  2003. status = item['status']
  2004. missing = package not in packages
  2005. important = self.ITEM_DESCRIPTION.get(status) is not None
  2006. if important or missing:
  2007. packages[package] = status
  2008. return packages
  2009. def update_action_item(self, package, status):
  2010. description = self.ITEM_DESCRIPTION.get(status)
  2011. if not description: # Not worth an action item
  2012. return False
  2013. action_item = package.get_action_item_for_type(
  2014. self.action_item_type.type_name)
  2015. if action_item is None:
  2016. action_item = ActionItem(
  2017. package=package,
  2018. item_type=self.action_item_type,
  2019. severity=ActionItem.SEVERITY_NORMAL)
  2020. url = "{}/debian/rb-pkg/{}.html".format(self.BASE_URL, package.name)
  2021. action_item.short_description = description.format(url=url)
  2022. action_item.save()
  2023. return True
  2024. def execute(self):
  2025. reproducibilities = self.get_build_reproducibility()
  2026. if reproducibilities is None:
  2027. return
  2028. with transaction.atomic():
  2029. PackageExtractedInfo.objects.filter(key='reproducibility').delete()
  2030. packages = []
  2031. extracted_info = []
  2032. for name, status in reproducibilities.items():
  2033. try:
  2034. package = SourcePackageName.objects.get(name=name)
  2035. if self.update_action_item(package, status):
  2036. packages.append(package)
  2037. except SourcePackageName.DoesNotExist:
  2038. continue
  2039. reproducibility_info = PackageExtractedInfo(
  2040. key='reproducibility',
  2041. package=package,
  2042. value={'reproducibility': status})
  2043. extracted_info.append(reproducibility_info)
  2044. ActionItem.objects.delete_obsolete_items([self.action_item_type],
  2045. packages)
  2046. PackageExtractedInfo.objects.bulk_create(extracted_info)
  2047. class MultiArchHintsTask(BaseTask):
  2048. ACTIONS_WEB = 'https://wiki.debian.org/MultiArch/Hints'
  2049. ACTIONS_URL = 'https://dedup.debian.net/static/multiarch-hints.yaml'
  2050. ACTION_ITEM_TYPE_NAME = 'debian-multiarch-hints'
  2051. ACTION_ITEM_TEMPLATE = 'debian/multiarch-hints.html'
  2052. ACTION_ITEM_DESCRIPTION = \
  2053. '<a href="{link}">Multiarch hinter</a> reports {count} issue(s)'
  2054. def __init__(self, force_update=False, *args, **kwargs):
  2055. super(MultiArchHintsTask, self).__init__(*args, **kwargs)
  2056. self.force_update = force_update
  2057. self.action_item_type = ActionItemType.objects.create_or_update(
  2058. type_name=self.ACTION_ITEM_TYPE_NAME,
  2059. full_description_template=self.ACTION_ITEM_TEMPLATE)
  2060. self.SEVERITIES = {}
  2061. for value, name in ActionItem.SEVERITIES:
  2062. self.SEVERITIES[name] = value
  2063. def set_parameters(self, parameters):
  2064. if 'force_update' in parameters:
  2065. self.force_update = parameters['force_update']
  2066. def get_data(self):
  2067. data = get_resource_content(self.ACTIONS_URL)
  2068. data = yaml.safe_load(data)
  2069. return data
  2070. def get_packages(self):
  2071. data = self.get_data()
  2072. if data['format'] != 'multiarch-hints-1.0':
  2073. return None
  2074. data = data['hints']
  2075. packages = collections.defaultdict(dict)
  2076. for item in data:
  2077. if 'source' not in item:
  2078. continue
  2079. package = item['source']
  2080. wishlist = ActionItem.SEVERITY_WISHLIST
  2081. severity = self.SEVERITIES.get(item['severity'], wishlist)
  2082. pkg_severity = packages[package].get('severity', wishlist)
  2083. packages[package]['severity'] = max(severity, pkg_severity)
  2084. packages[package].setdefault('hints', []).append(
  2085. (item['description'], item['link']))
  2086. return packages
  2087. def update_action_item(self, package, severity, description, extra_data):
  2088. action_item = package.get_action_item_for_type(
  2089. self.action_item_type.type_name)
  2090. if action_item is None:
  2091. action_item = ActionItem(
  2092. package=package,
  2093. item_type=self.action_item_type)
  2094. action_item.severity = severity
  2095. action_item.short_description = description
  2096. action_item.extra_data = extra_data
  2097. action_item.save()
  2098. def execute(self):
  2099. packages = self.get_packages()
  2100. if not packages:
  2101. return
  2102. with transaction.atomic():
  2103. for name, data in packages.items():
  2104. try:
  2105. package = SourcePackageName.objects.get(name=name)
  2106. except SourcePackageName.DoesNotExist:
  2107. continue
  2108. description = self.ACTION_ITEM_DESCRIPTION.format(
  2109. count=len(data['hints']), link=self.ACTIONS_WEB)
  2110. self.update_action_item(package, data['severity'], description,
  2111. data['hints'])
  2112. ActionItem.objects.delete_obsolete_items([self.action_item_type],
  2113. packages.keys())