1
2
3
4
5
6
7
8
9
10
11
12
13
14
15 import os
16 import rpm
17 import time
18 import logging
19 import xmlrpclib
20 import turbogears
21
22 from sqlobject import *
23 from datetime import datetime
24
25 from turbogears import config, flash
26 from turbogears.database import PackageHub
27
28 from os.path import isfile, join
29 from textwrap import wrap
30
31 from bodhi import buildsys, mail
32 from bodhi.util import get_nvr, rpm_fileheader, header, get_age, get_age_in_days
33 from bodhi.exceptions import RPMNotFound
34 from bodhi.identity.tables import *
35
36 log = logging.getLogger(__name__)
37 hub = PackageHub("bodhi")
38 __connection__ = hub
39
40 soClasses=('Release', 'Package', 'PackageBuild', 'PackageUpdate', 'CVE',
41 'Bugzilla', 'Comment', 'User', 'Group', 'Visit')
42
44 """ Table of releases that we will be pushing updates for """
45 name = UnicodeCol(alternateID=True, notNone=True)
46 long_name = UnicodeCol(notNone=True)
47 updates = MultipleJoin('PackageUpdate', joinColumn='release_id')
48 id_prefix = UnicodeCol(notNone=True)
49 dist_tag = UnicodeCol(notNone=True)
50
52 name = UnicodeCol(alternateID=True, notNone=True)
53 builds = MultipleJoin('PackageBuild', joinColumn='package_id')
54 suggest_reboot = BoolCol(default=False)
55
60
62 i = 0
63 for build in self.builds:
64 i += len(build.updates)
65 return i
66
68 x = header(self.name)
69 states = { 'pending' : [], 'testing' : [], 'stable' : [] }
70 if len(self.builds):
71 for build in self.builds:
72 for state in states.keys():
73 states[state] += filter(lambda u: u.status == state,
74 build.updates)
75 for state in states.keys():
76 if len(states[state]):
77 x += "\n %s Updates (%d)\n" % (state.title(),
78 len(states[state]))
79 for update in states[state]:
80 x += " o %s\n" % update.title
81 return x
82
84 nvr = UnicodeCol(notNone=True, alternateID=True)
85 package = ForeignKey('Package')
86 updates = RelatedJoin("PackageUpdate")
87
91
93 """
94 Retrieve the RPM changelog of this package since it's last update
95 """
96 rpm_header = self.get_rpm_header()
97 descrip = rpm_header[rpm.RPMTAG_CHANGELOGTEXT]
98 if not descrip: return ""
99
100 who = rpm_header[rpm.RPMTAG_CHANGELOGNAME]
101 when = rpm_header[rpm.RPMTAG_CHANGELOGTIME]
102
103 num = len(descrip)
104 if num == 1: when = [when]
105
106 str = ""
107 i = 0
108 while (i < num) and (when[i] > timelimit):
109 str += '* %s %s\n%s\n' % (time.strftime("%a %b %e %Y",
110 time.localtime(when[i])), who[i],
111 descrip[i])
112 i += 1
113 return str
114
116 """ Return the path to the SRPM for this update """
117 src_path = self.get_source_path()
118 path = src_path.split('/')
119 srpm = join(src_path, "src", "%s.src.rpm" % ('-'.join(path[-3:])))
120 if not isfile(srpm):
121 log.debug("Cannot find SRPM: %s" % srpm)
122 raise RPMNotFound
123 return srpm
124
126 """ Return the path of this built update """
127 return join(config.get('build_dir'), *get_nvr(self.nvr))
128
130 """ Return the path to the last released srpm of this package """
131 latest_srpm = None
132 koji_session = buildsys.get_session()
133
134
135
136
137
138
139
140 nvr = get_nvr(self.nvr)
141 for tag in ['%s-updates', '%s']:
142 tag %= self.updates[0].release.dist_tag
143 builds = koji_session.getLatestBuilds(tag, None, self.package.name)
144 latest = None
145
146
147 for build in builds:
148 if rpm.labelCompare(nvr, get_nvr(build['nvr'])) > 0:
149 latest = get_nvr(build['nvr'])
150 break
151
152 if latest:
153 srpm_path = join(config.get('build_dir'), latest[0],
154 latest[1], latest[2], 'src',
155 '%s.src.rpm' % '-'.join(latest))
156 if isfile(srpm_path):
157 log.debug("Latest build before %s: %s" % (self.nvr,
158 srpm_path))
159 latest_srpm = srpm_path
160 else:
161 log.warning("Latest build %s not found" % srpm_path)
162 break
163
164 return latest_srpm
165
167 """ This class defines an update in our system. """
168 title = UnicodeCol(notNone=True, alternateID=True, unique=True)
169 builds = RelatedJoin("PackageBuild")
170 date_submitted = DateTimeCol(default=datetime.utcnow, notNone=True)
171 date_modified = DateTimeCol(default=None)
172 date_pushed = DateTimeCol(default=None)
173 submitter = UnicodeCol(notNone=True)
174 update_id = UnicodeCol(default=None)
175 type = EnumCol(enumValues=['security', 'bugfix', 'enhancement'])
176 cves = RelatedJoin("CVE")
177 bugs = RelatedJoin("Bugzilla")
178 release = ForeignKey('Release')
179 status = EnumCol(enumValues=['pending', 'testing', 'stable',
180 'obsolete'], default='pending')
181 pushed = BoolCol(default=False)
182 notes = UnicodeCol()
183 request = EnumCol(enumValues=['testing', 'stable', 'obsolete',
184 None], default=None)
185 comments = MultipleJoin('Comment', joinColumn='update_id')
186 karma = IntCol(default=0)
187 close_bugs = BoolCol(default=True)
188 nagged = PickleCol(default=None)
189
191 return delim.join([build.nvr for build in self.builds])
192
194 """ Return a space-delimited string of bug numbers for this update """
195 val = ''
196 if show_titles:
197 i = 0
198 for bug in self.bugs:
199 bugstr = '%s%s - %s\n' % (i and ' ' * 11 + ': ' or '',
200 bug.bz_id, bug.title)
201 val += '\n'.join(wrap(bugstr, width=67,
202 subsequent_indent=' ' * 11 + ': ')) + '\n'
203 i += 1
204 val = val[:-1]
205 else:
206 val = ' '.join([str(bug.bz_id) for bug in self.bugs])
207 return val
208
210 """ Return a space-delimited string of CVE ids for this update """
211 return ' '.join([cve.cve_id for cve in self.cves])
212
214 """
215 Assign an update ID to this update. This function finds the next number
216 in the sequence of pushed updates for this release, increments it and
217 prefixes it with the id_prefix of the release and the year
218 (ie FEDORA-2007-0001)
219 """
220 if self.update_id != None and self.update_id != u'None':
221 log.debug("Keeping current update id %s" % self.update_id)
222 return
223 update = PackageUpdate.select(PackageUpdate.q.update_id != 'None',
224 orderBy=PackageUpdate.q.update_id)
225 try:
226 id = int(update[-1].update_id.split('-')[-1]) + 1
227 except (AttributeError, IndexError):
228 id = 1
229 self.update_id = u'%s-%s-%0.4d' % (self.release.id_prefix,
230 time.localtime()[0],id)
231 log.debug("Setting update_id for %s to %s" % (self.title,
232 self.update_id))
233 hub.commit()
234
267
269 import turbomail
270 log.debug("Sending update notice for %s" % self.title)
271 mailinglist = None
272 sender = config.get('bodhi_email')
273 if not sender:
274 log.error("bodhi_email not defined in configuration! Unable " +
275 "to send update notice")
276 return
277 if self.status == 'stable':
278 mailinglist = config.get('%s_announce_list' %
279 self.release.id_prefix.lower())
280 elif self.status == 'testing':
281 mailinglist = config.get('%s_test_announce_list' %
282 self.release.id_prefix.lower())
283 if mailinglist:
284 for subject, body in mail.get_template(self):
285 message = turbomail.Message(sender, mailinglist, subject)
286 message.plain = body
287 try:
288 turbomail.enqueue(message)
289 log.debug("Sending mail: %s" % message.plain)
290 except turbomail.MailNotEnabledException:
291 log.warning("mail.on is not True!")
292 else:
293 log.error("Cannot find mailing list address for update notice")
294
300
302 """
303 Return a string representation of this update.
304 """
305 val = header(self.title.replace(',', ', '))
306 if self.update_id:
307 val += " Update ID: %s\n" % self.update_id
308 val += """ Release: %s
309 Status: %s
310 Type: %s
311 Karma: %d""" % (self.release.long_name,self.status,self.type,self.karma)
312 if self.request != None:
313 val += "\n Request: %s" % self.request
314 if len(self.bugs):
315 bugs = self.get_bugstring(show_titles=True)
316 val += "\n Bugs: %s" % bugs
317 if len(self.cves):
318 val += "\n CVEs: %s" % self.get_cvestring()
319 if self.notes:
320 notes = wrap(self.notes, width=67, subsequent_indent=' '*11 +': ')
321 val += "\n Notes: %s" % '\n'.join(notes)
322 val += """
323 Submitter: %s
324 Submitted: %s\n\n %s
325 """ % (self.submitter, self.date_submitted,
326 config.get('base_address') + turbogears.url(self.get_url()))
327 return val.rstrip()
328
330 """
331 Get the tag that this build is currently tagged with.
332 TODO: we should probably get this stuff from koji instead of guessing
333 """
334 tag = '%s-updates' % self.release.dist_tag
335 if self.status == 'pending':
336 tag += '-candidate'
337 elif self.status == 'testing':
338 tag += '-testing'
339 return tag
340
342 """
343 Create any new bugs, and remove any missing ones. Destroy removed bugs
344 that are no longer referenced anymore
345 """
346 for bug in self.bugs:
347 if bug.bz_id not in bugs:
348 self.removeBugzilla(bug)
349 if len(bug.updates) == 0:
350 log.debug("Destroying stray Bugzilla #%d" % bug.bz_id)
351 bug.destroySelf()
352 for bug in bugs:
353 try:
354 bz = Bugzilla.byBz_id(bug)
355 if bz not in self.bugs:
356 self.addBugzilla(bz)
357 except SQLObjectNotFound:
358 bz = Bugzilla(bz_id=bug)
359 self.addBugzilla(bz)
360
362 """
363 Create any new CVES, and remove any missing ones. Destroy removed CVES
364 that are no longer referenced anymore.
365 """
366 for cve in self.cves:
367 if cve.cve_id not in cves:
368 log.debug("Removing CVE %s from %s" % (cve.cve_id, self.title))
369 self.removeCVE(cve)
370 if cve.cve_id not in cves and len(cve.updates) == 0:
371 log.debug("Destroying stray CVE #%s" % cve.cve_id)
372 cve.destroySelf()
373 for cve_id in cves:
374 try:
375 cve = CVE.byCve_id(cve_id)
376 if cve not in self.cves:
377 self.addCVE(cve)
378 except SQLObjectNotFound:
379 log.debug("Creating new CVE: %s" % cve_id)
380 cve = CVE(cve_id=cve_id)
381 self.addCVE(cve)
382
385
389 age = get_age_in_days(self.date_pushed)
390 if age == 0:
391 color = '#ff0000'
392 elif age < 4:
393 color = '#ff6600'
394 elif age < 7:
395 color = '#ffff00'
396 else:
397 color = '#00ff00'
398 return color
399
429
443
444 -class CVE(SQLObject):
445 """ Table of CVEs fixed within updates that we know of. """
446 cve_id = UnicodeCol(alternateID=True, notNone=True)
447 updates = RelatedJoin("PackageUpdate")
448
450 return "http://www.cve.mitre.org/cgi-bin/cvename.cgi?name=%s" % self.cve_id
451
453 """ Table of Bugzillas that we know about. """
454 bz_id = IntCol(alternateID=True)
455 title = UnicodeCol(default=None)
456 updates = RelatedJoin("PackageUpdate")
457 security = BoolCol(default=False)
458
459 _bz_server = config.get("bz_server")
460 default_msg = "%s has been pushed to the %s repository. If problems " + \
461 "still persist, please make note of it in this bug report."
462
464 """
465 When the ID for this bug is set (upon creation), go out and fetch the
466 details and check if this bug is security related.
467 """
468 self._SO_set_bz_id(bz_id)
469 self._fetch_details()
470
472 try:
473 log.debug("Fetching bugzilla #%d" % self.bz_id)
474 server = xmlrpclib.Server(self._bz_server)
475 me = config.get('bodhi_email')
476 password = config.get('bodhi_password')
477 if not password:
478 log.error("No password stored for bodhi_email")
479 return
480 bug = server.bugzilla.getBug(self.bz_id, me, password)
481 del server
482 self.title = bug['short_desc']
483 if bug['keywords'].lower().find('security') != -1:
484 self.security = True
485 except xmlrpclib.Fault, f:
486 self.title = 'Invalid bug number'
487 log.warning("Got fault from Bugzilla: %s" % str(f))
488 except Exception, e:
489 self.title = 'Unable to fetch bug title'
490 log.error(self.title + ': ' + str(e))
491
502
519
521 me = config.get('bodhi_email')
522 password = config.get('bodhi_password')
523 if password:
524 log.debug("Closing Bug #%d" % self.bz_id)
525 ver = '-'.join(get_nvr(update.builds[0].nvr)[-2:])
526 try:
527 server = xmlrpclib.Server(self._bz_server)
528 server.bugzilla.closeBug(self.bz_id, 'ERRATA', me,
529 password, 0, ver)
530 del server
531 except Exception, e:
532 log.error("Cannot close bug #%d" % self.bz_id)
533 else:
534 log.warning("bodhi_password not defined; unable to close bug")
535
537 return "https://bugzilla.redhat.com/show_bug.cgi?id=%s" % self.bz_id
538
539
540 global _releases
541 _releases = None
549