1
2
3
4
5
6
7
8
9
10
11
12
13
14
15 import sys
16 import rpm
17 import mail
18 import time
19 import logging
20 import cherrypy
21
22 from koji import GenericError
23 from datetime import datetime
24 from sqlobject import SQLObjectNotFound
25 from sqlobject.sqlbuilder import AND, OR
26
27 from turbogears import (controllers, expose, validate, redirect, identity,
28 paginate, flash, error_handler, validators, config, url,
29 exception_handler)
30 from turbogears.widgets import DataGrid, Tabber
31
32 from bodhi import buildsys, util
33 from bodhi.rss import Feed
34 from bodhi.util import flash_log, get_pkg_people
35 from bodhi.new import NewUpdateController, update_form
36 from bodhi.admin import AdminController
37 from bodhi.model import (Package, PackageBuild, PackageUpdate, Release,
38 Bugzilla, CVE, Comment)
39 from bodhi.search import SearchController
40 from bodhi.widgets import CommentForm, OkCancelForm
41 from bodhi.exceptions import (RPMNotFound, DuplicateEntryError,
42 PostgresIntegrityError, SQLiteIntegrityError)
43
44 from os.path import isfile, join
45
46 log = logging.getLogger(__name__)
47
48
49 -class Root(controllers.RootController):
50
51 new = NewUpdateController()
52 admin = AdminController()
53 search = SearchController()
54 rss = Feed("rss2.0")
55
56 comment_form = CommentForm()
57 ok_cancel_form = OkCancelForm()
58
60 """ Generic exception handler """
61 log.error("Exception thrown: %s" % str(tg_exceptions))
62 flash_log(str(tg_exceptions))
63 if 'tg_format' in cherrypy.request.params and \
64 cherrypy.request.params['tg_format'] == 'json':
65 return dict()
66 raise redirect("/")
67
71
72 @identity.require(identity.not_anonymous())
73 @expose(template='bodhi.templates.welcome')
75 """
76 The main dashboard. Here we generate the Tabber and all of the
77 DataGrids for the various tabs.
78 """
79 from bodhi.util import make_update_link, make_type_icon, make_karma_icon
80 RESULTS, FIELDS, GRID = range(3)
81 tabs = Tabber()
82
83
84 grids = {
85 'Comments' : [
86 Comment.select(orderBy=Comment.q.timestamp).reversed(),
87 [
88 ('Update', make_update_link),
89 ('From', lambda row: row.author),
90 ('Comment', lambda row: row.text),
91 ('Karma', make_karma_icon)
92 ]
93 ],
94 'Mine' : [
95 PackageUpdate.select(
96 PackageUpdate.q.submitter == identity.current.user_name,
97 orderBy=PackageUpdate.q.date_pushed
98 ).reversed(),
99 [
100 ('Name', make_update_link),
101 ('Type', make_type_icon),
102 ('Status', lambda row: row.status),
103 ('Age', lambda row: row.get_submitted_age()),
104 ('Karma', make_karma_icon)
105 ]
106 ],
107 'Testing' : [
108 PackageUpdate.select(
109 PackageUpdate.q.status == 'testing',
110 orderBy=PackageUpdate.q.date_pushed
111 ).reversed(),
112 [
113 ('Name', make_update_link),
114 ('Type', make_type_icon),
115 ('Submitter', lambda row: row.submitter),
116 ('Age', lambda row: row.get_pushed_age()),
117 ('Karma', make_karma_icon)
118 ]
119 ],
120 'Stable' : [
121 PackageUpdate.select(
122 PackageUpdate.q.status == 'stable',
123 orderBy=PackageUpdate.q.date_pushed
124 ).reversed(),
125 [
126 ('Name', make_update_link),
127 ('Update ID', lambda row: row.update_id),
128 ('Type', make_type_icon),
129 ('Submitter', lambda row: row.submitter),
130 ('Age', lambda row: row.get_pushed_age())
131 ]
132 ],
133 'Security' : [
134 PackageUpdate.select(
135 AND(PackageUpdate.q.type == 'security',
136 PackageUpdate.q.status == 'stable'),
137 orderBy=PackageUpdate.q.date_pushed
138 ).reversed(),
139 [
140 ('Name', make_update_link),
141 ('Update ID', lambda row: row.update_id),
142 ('Submitter', lambda row: row.submitter),
143 ('Age', lambda row: row.get_pushed_age())
144 ]
145 ]
146 }
147
148 for key, value in grids.items():
149 if not value[RESULTS].count():
150 grids[key].append(None)
151 continue
152 if value[RESULTS].count() > 5:
153 value[RESULTS] = value[RESULTS][:5]
154 value[RESULTS] = list(value[RESULTS])
155
156 grids[key].append(DataGrid(fields=value[FIELDS],
157 default=value[RESULTS]))
158
159 return dict(now=time.ctime(), grids=grids, tabs=tabs)
160
161 @expose(template='bodhi.templates.pkgs')
162 @paginate('pkgs', default_order='name', limit=20, allow_limit_override=True)
166
167 @expose(template="bodhi.templates.login", allow_json=True)
168 - def login(self, forward_url=None, previous_url=None, *args, **kw):
169 if not identity.current.anonymous and identity.was_login_attempted() \
170 and not identity.get_identity_errors():
171 if 'tg_format' in cherrypy.request.params and \
172 cherrypy.request.params['tg_format'] == 'json':
173 return dict(user=identity.current.user)
174 raise redirect(forward_url)
175
176 forward_url=None
177 previous_url= cherrypy.request.path
178
179 if identity.was_login_attempted():
180 msg=_("The credentials you supplied were not correct or "
181 "did not grant access to this resource.")
182 elif identity.get_identity_errors():
183 msg=_("You must provide your credentials before accessing "
184 "this resource.")
185 else:
186 msg=_("Please log in.")
187 forward_url= cherrypy.request.headers.get("Referer", "/")
188
189 cherrypy.response.status=403
190 return dict(message=msg, previous_url=previous_url, logging_in=True,
191 original_parameters=cherrypy.request.params,
192 forward_url=forward_url)
193 @expose()
197
198 @expose(template="bodhi.templates.list", allow_json=True)
199 @paginate('updates', limit=20, allow_limit_override=True)
200 - def list(self, release=None, bugs=None, cves=None, status=None, type=None):
201 """ Return a list of updates based on given parameters """
202 log.debug("list(%s, %s, %s, %s, %s)" % (release, bugs, cves, status,
203 type))
204 query = []
205 if release:
206 rel = Release.byName(release)
207 query.append(PackageUpdate.q.releaseID == rel.id)
208 if status:
209 query.append(PackageUpdate.q.status == status)
210 if type:
211 query.append(PackageUpdate.q.type == type)
212
213 updates = PackageUpdate.select(AND(*query))
214 num_items = updates.count()
215
216
217 results = []
218 if bugs:
219 try:
220 for bug in map(Bugzilla.byBz_id, map(int, bugs.split(','))):
221 map(results.append,
222 filter(lambda x: bug in x.bugs, updates))
223 except SQLObjectNotFound, e:
224 flash_log(e)
225 if self.jsonRequest():
226 return dict(updates=[])
227 updates = results
228 num_items = len(updates)
229 if cves:
230 try:
231 for cve in map(CVE.byCve_id, cves.split(',')):
232 map(results.append,
233 filter(lambda x: cve in x.cves, updates))
234 except SQLObjectNotFound, e:
235 flash_log(e)
236 if self.jsonRequest():
237 return dict(updates=[])
238 updates = results
239 num_items = len(updates)
240
241 if self.jsonRequest():
242 updates = map(str, updates)
243
244 return dict(updates=updates, num_items=num_items)
245
246 @expose(template="bodhi.templates.list")
247 @identity.require(identity.not_anonymous())
248 @paginate('updates', limit=20, allow_limit_override=True)
257
258 @identity.require(identity.not_anonymous())
259 @expose(template='bodhi.templates.show')
260 - def show(self, update):
264
265 @expose()
266 @identity.require(identity.not_anonymous())
277
278 @exception_handler(exception)
279 @expose(allow_json=True)
280 @identity.require(identity.not_anonymous())
281 - def move(self, nvr):
299
300 @exception_handler(exception)
301 @expose(allow_json=True)
302 @identity.require(identity.not_anonymous())
303 - def push(self, nvr):
322
323 @expose()
324 @identity.require(identity.not_anonymous())
336
337 @exception_handler(exception)
338 @expose(allow_json=True)
339 @identity.require(identity.not_anonymous())
358
359 @identity.require(identity.not_anonymous())
360 @expose(template='bodhi.templates.form')
361 - def edit(self, update):
362 """ Edit an update """
363 update = PackageUpdate.byTitle(update)
364 if not util.authorized_user(update, identity):
365 flash_log("Cannot edit an update you did not submit")
366 raise redirect(update.get_url())
367 values = {
368 'builds' : {'text':update.title, 'hidden':update.title},
369 'release' : update.release.long_name,
370 'testing' : update.status == 'testing',
371 'type' : update.type,
372 'notes' : update.notes,
373 'bugs' : update.get_bugstring(),
374 'cves' : update.get_cvestring(),
375 'edited' : update.title
376 }
377 return dict(form=update_form, values=values, action=url("/save"))
378
379 @expose(allow_json=True)
380 @error_handler(new.index)
381 @validate(form=update_form)
382 @identity.require(identity.not_anonymous())
383 - def save(self, builds, release, type, cves, notes, bugs, close_bugs=False,
384 edited=False, **kw):
385 """
386 Save an update. This includes new updates and edited.
387 """
388 log.debug("save(%s, %s, %s, %s, %s, %s, %s, %s, %s)" % (builds, release,
389 type, cves, notes, bugs, close_bugs, edited, kw))
390
391 note = ''
392 update_builds = []
393 if not cves: cves = []
394 if not bugs: bugs = []
395 release = Release.select(
396 OR(Release.q.long_name == release,
397 Release.q.name == release))[0]
398
399
400 params = {
401 'builds.text' : ' '.join(builds),
402 'release' : release.long_name,
403 'type' : type,
404 'cves' : ' '.join(cves),
405 'bugs' : ' '.join(map(str, bugs)),
406 'notes' : notes,
407 'close_bugs' : close_bugs and 'True' or '',
408 'edited' : edited
409 }
410
411
412 for build in builds:
413 nvr = util.get_nvr(build)
414 people = get_pkg_people(nvr[0], release.long_name.split()[0],
415 release.long_name[-1])
416 if not identity.current.user_name in people[0] and \
417 not 'releng' in identity.current.groups:
418 flash_log("%s does not have commit access to %s" % (
419 identity.current.user_name, nvr[0]))
420 raise redirect('/new', **params)
421
422
423
424
425 if edited:
426 update = PackageUpdate.byTitle(edited)
427 if update.status in ('testing', 'stable'):
428 if filter(lambda build: build not in edited, builds) or \
429 filter(lambda build: build not in builds, edited.split()):
430 flash_log("You must unpush this update before you can "
431 "add or remove any builds.")
432 raise redirect(update.get_url())
433 map(lambda build: build.destroySelf(), update.builds)
434
435
436 koji = buildsys.get_session()
437 for build in builds:
438 log.debug("Validating koji tag for %s" % build)
439 tag_matches = False
440 candidate = '%s-updates-candidate' % release.dist_tag
441 try:
442 for tag in koji.listTags(build):
443 if tag['name'] == candidate:
444 log.debug("%s built with tag %s" % (build, tag['name']))
445 tag_matches = True
446 break
447 except GenericError, e:
448 flash_log("Invalid build: %s" % build)
449 if self.jsonRequest():
450 return dict()
451 raise redirect('/new', **params)
452 if not tag_matches:
453 flash_log("%s build is not tagged with %s" % (build, candidate))
454 if self.jsonRequest():
455 return dict()
456 raise redirect('/new', **params)
457
458
459 nvr = util.get_nvr(build)
460 try:
461 package = Package.byName(nvr[0])
462 except SQLObjectNotFound:
463 package = Package(name=nvr[0])
464
465
466 tag = release.dist_tag
467 while True:
468 try:
469 for kojiTag in (tag, tag + '-updates'):
470 log.debug("Checking for broken update paths in " + tag)
471 for kojiBuild in koji.listTagged(kojiTag,
472 package=nvr[0]):
473 buildNvr = util.get_nvr(kojiBuild['nvr'])
474 if rpm.labelCompare(nvr, buildNvr) < 0:
475 msg = "Broken update path: %s is older than " \
476 "update %s in %s" % (build,
477 kojiBuild['nvr'],
478 kojiTag)
479 flash_log(msg)
480 raise redirect('/new', **params)
481 except GenericError:
482 break
483
484
485 tag = tag[:-1] + str(int(tag[-1]) - 1)
486
487 try:
488 pkgBuild = PackageBuild(nvr=build, package=package)
489 update_builds.append(pkgBuild)
490 except (PostgresIntegrityError, SQLiteIntegrityError,
491 DuplicateEntryError):
492 flash_log("Update for %s already exists" % build)
493 if self.jsonRequest():
494 return dict()
495 raise redirect('/new', **params)
496
497
498 if edited:
499 p = PackageUpdate.byTitle(edited)
500 try:
501 p.set(release=release, date_modified=datetime.utcnow(),
502 notes=notes, type=type, title=','.join(builds),
503 close_bugs=close_bugs)
504 log.debug("Edited update %s" % edited)
505 except (DuplicateEntryError, PostgresIntegrityError,
506 SQLiteIntegrityError):
507 flash_log("Update already exists for build in: %s" %
508 ' '.join(builds))
509 if self.jsonRequest():
510 return dict()
511 raise redirect('/new', **params)
512 else:
513 try:
514 p = PackageUpdate(title=','.join(builds), release=release,
515 submitter=identity.current.user_name,
516 notes=notes, type=type, close_bugs=close_bugs)
517 log.info("Adding new update %s" % builds)
518 except (PostgresIntegrityError, SQLiteIntegrityError,
519 DuplicateEntryError):
520 flash_log("Update for %s already exists" % builds)
521 if self.jsonRequest():
522 return dict()
523 raise redirect('/new', **params)
524
525
526 map(p.addPackageBuild, update_builds)
527
528
529 p.update_bugs(bugs)
530 p.update_cves(cves)
531
532
533
534 if p.type != 'security':
535 for bug in p.bugs:
536 if bug.security:
537 p.type = 'security'
538 note += '; Security bug provided, changed update type ' + \
539 'to security'
540 break
541 if p.cves != [] and (p.type != 'security'):
542 p.type = 'security'
543 note += '; CVEs provided, changed update type to security'
544
545 if edited:
546 mail.send(p.submitter, 'edited', p)
547 flash_log("Update successfully edited" + note)
548 else:
549
550 if p.type == 'security':
551 mail.send(config.get('security_team'), 'new', p)
552 mail.send(p.submitter, 'new', p)
553 flash_log("Update successfully created" + note)
554
555
556 if self.jsonRequest():
557 return dict(update=str(p))
558
559 raise redirect(p.get_url())
560
561 @expose(template='bodhi.templates.list')
562 @identity.require(identity.not_anonymous())
563 @paginate('updates', limit=20, allow_limit_override=True)
565 """
566 This method allows for /[(pending|testing)/]<release>[/<update>]
567 requests.
568 """
569 args = [arg for arg in args]
570 status = 'stable'
571 order = PackageUpdate.q.date_pushed
572 template = 'bodhi.templates.list'
573
574 if len(args) and args[0] == 'testing':
575 status = 'testing'
576 template = 'bodhi.templates.testing'
577 del args[0]
578 if len(args) and args[0] == 'pending':
579 status = 'pending'
580 template = 'bodhi.templates.pending'
581 order = PackageUpdate.q.date_submitted
582 del args[0]
583 if not len(args):
584 updates = PackageUpdate.select(PackageUpdate.q.status == status,
585 orderBy=order).reversed()
586 return dict(updates=updates, tg_template=template,
587 num_items=updates.count())
588
589 try:
590 release = Release.byName(args[0])
591 try:
592 update = PackageUpdate.select(
593 AND(PackageUpdate.q.releaseID == release.id,
594 PackageUpdate.q.title == args[1],
595 PackageUpdate.q.status == status))[0]
596 update.comments.sort(lambda x, y: cmp(x.timestamp, y.timestamp))
597 return dict(tg_template='bodhi.templates.show',
598 update=update, updates=[],
599 comment_form=self.comment_form,
600 values={'title' : update.title})
601 except SQLObjectNotFound:
602 flash_log("Update %s not found" % args[1])
603 raise redirect('/')
604 except IndexError:
605 updates = PackageUpdate.select(
606 AND(PackageUpdate.q.releaseID == release.id,
607 PackageUpdate.q.status == status),
608 orderBy=order).reversed()
609 return dict(updates=updates, num_items=updates.count(),
610 tg_template=template,
611 title='%s %s Updates' % (release.long_name,
612 status.title()))
613 except SQLObjectNotFound:
614 pass
615
616
617 try:
618 pkg = Package.byName(args[0])
619 return dict(tg_template='bodhi.templates.pkg', pkg=pkg, updates=[])
620 except SQLObjectNotFound:
621 pass
622
623 flash_log("The path %s cannot be found" % cherrypy.request.path)
624 raise redirect("/")
625
626 @expose()
627 @error_handler()
628 @validate(form=comment_form)
629 @validate(validators={ 'karma' : validators.Int() })
630 @identity.require(identity.not_anonymous())
638
639 @expose(template='bodhi.templates.confirmation')
649