Package bodhi :: Module controllers
[hide private]
[frames] | no frames]

Source Code for Module bodhi.controllers

  1  # $Id: controllers.py,v 1.11 2007/01/08 06:07:07 lmacken Exp $ 
  2  # This program is free software; you can redistribute it and/or modify 
  3  # it under the terms of the GNU General Public License as published by 
  4  # the Free Software Foundation; version 2 of the License. 
  5  # 
  6  # This program is distributed in the hope that it will be useful, 
  7  # but WITHOUT ANY WARRANTY; without even the implied warranty of 
  8  # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the 
  9  # GNU Library General Public License for more details. 
 10  # 
 11  # You should have received a copy of the GNU General Public License 
 12  # along with this program; if not, write to the Free Software 
 13  # Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. 
 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
59 - def exception(self, tg_exceptions=None):
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
68 - def jsonRequest(self):
69 return 'tg_format' in cherrypy.request.params and \ 70 cherrypy.request.params['tg_format'] == 'json'
71 72 @identity.require(identity.not_anonymous()) 73 @expose(template='bodhi.templates.welcome')
74 - def index(self):
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 # { 'Title' : [SelectResults, [(row, row_callback),]], ... } 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)
163 - def pkgs(self):
164 pkgs = Package.select() 165 return dict(pkgs=pkgs, num_pkgs=pkgs.count())
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()
194 - def logout(self):
195 identity.current.logout() 196 raise redirect('/')
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 # Filter results by Bugs and/or CVEs 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)
249 - def mine(self):
250 """ List all updates submitted by the current user """ 251 updates = PackageUpdate.select( 252 OR(PackageUpdate.q.submitter == util.displayname(identity), 253 PackageUpdate.q.submitter == identity.current.user_name), 254 orderBy=PackageUpdate.q.date_pushed).reversed() 255 return dict(updates=updates, num_items=updates.count(), 256 title='%s\'s updates' % identity.current.user_name)
257 258 @identity.require(identity.not_anonymous()) 259 @expose(template='bodhi.templates.show')
260 - def show(self, update):
261 update = PackageUpdate.byTitle(update) 262 update.comments.sort(lambda x, y: cmp(x.timestamp, y.timestamp)) 263 return dict(update=update, comment_form=self.comment_form)
264 265 @expose() 266 @identity.require(identity.not_anonymous())
267 - def revoke(self, nvr):
268 """ Revoke a push request for a specified update """ 269 update = PackageUpdate.byTitle(nvr) 270 if not util.authorized_user(update, identity): 271 flash_log("Cannot revoke an update you did not submit") 272 raise redirect(update.get_url()) 273 flash_log("%s request revoked" % update.request.title()) 274 mail.send_admin('revoke', update) 275 update.request = None 276 raise redirect(update.get_url())
277 278 @exception_handler(exception) 279 @expose(allow_json=True) 280 @identity.require(identity.not_anonymous())
281 - def move(self, nvr):
282 update = PackageUpdate.byTitle(nvr) 283 # Test if package already has been pushed (posible when called json) 284 if not update.status in ['pending','testing'] or \ 285 update.request in ["testing", "stable"]: 286 flash_log("Update is already pushed") 287 if self.jsonRequest(): return dict() 288 raise redirect(update.get_url()) 289 if not util.authorized_user(update, identity): 290 flash_log("Cannot move an update you did not submit") 291 if self.jsonRequest(): return dict() 292 raise redirect(update.get_url()) 293 update.request = 'stable' 294 flash_log("Requested that %s be pushed to %s-updates" % (nvr, 295 update.release.name)) 296 mail.send_admin('move', update) 297 if self.jsonRequest(): return dict() 298 raise redirect(update.get_url())
299 300 @exception_handler(exception) 301 @expose(allow_json=True) 302 @identity.require(identity.not_anonymous())
303 - def push(self, nvr):
304 """ Submit an update for pushing """ 305 update = PackageUpdate.byTitle(nvr) 306 # Test if package already has been pushed (posible when called json) 307 if update.status != 'pending' or update.request in ["testing","stable"]: 308 flash_log("Update is already pushed") 309 if self.jsonRequest(): return dict() 310 raise redirect(update.get_url()) 311 if not util.authorized_user(update, identity): 312 flash_log("Cannot push an update you did not submit") 313 if self.jsonRequest(): return dict() 314 raise redirect(update.get_url()) 315 update.request = 'testing' 316 repo = '%s-updates-testing' % update.release.name 317 msg = "%s has been submitted for pushing to %s" % (nvr, repo) 318 flash_log(msg) 319 mail.send_admin('push', update) 320 if self.jsonRequest(): return dict() 321 raise redirect(update.get_url())
322 323 @expose() 324 @identity.require(identity.not_anonymous())
325 - def unpush(self, nvr):
326 """ Submit an update for unpushing """ 327 update = PackageUpdate.byTitle(nvr) 328 if not util.authorized_user(update, identity): 329 flash_log("Cannot unpush an update you did not submit") 330 raise redirect(update.get_url()) 331 update.request = 'obsolete' 332 msg = "%s has been submitted for unpushing" % nvr 333 flash_log(msg) 334 mail.send_admin('unpush', update) 335 raise redirect(update.get_url())
336 337 @exception_handler(exception) 338 @expose(allow_json=True) 339 @identity.require(identity.not_anonymous())
340 - def delete(self, update):
341 """ Delete a pending update """ 342 update = PackageUpdate.byTitle(update) 343 if not util.authorized_user(update, identity): 344 flash_log("Cannot delete an update you did not submit") 345 if self.jsonRequest(): return dict() 346 raise redirect(update.get_url()) 347 if not update.pushed: 348 mail.send_admin('deleted', update) 349 msg = "Deleted %s" % update.title 350 map(lambda x: x.destroySelf(), update.comments) 351 map(lambda x: x.destroySelf(), update.builds) 352 update.destroySelf() 353 flash_log(msg) 354 else: 355 flash_log("Cannot delete a pushed update") 356 if self.jsonRequest(): return dict() 357 raise redirect("/pending")
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 # Parameters used to re-populate the update form if something fails 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 # Make sure the submitter has commit access to these builds 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 # Disallow adding or removing of builds when an update is testing or 423 # stable. If we're in a pending state, we destroy them all and 424 # create them later -- to allow for adding/removing of builds. 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 # Make sure the selected release matches the Koji tag for this build 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 # Get the package; if it doesn't exist, create it. 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 # Check for broken update paths against all previous releases 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 # Check against the previous release (until one doesn't exist) 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 # Modify or create the PackageUpdate 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 # Add the PackageBuilds to our PackageUpdate 526 map(p.addPackageBuild, update_builds) 527 528 # Add/remove the necessary Bugzillas and CVEs 529 p.update_bugs(bugs) 530 p.update_cves(cves) 531 532 # If there are any CVEs or security bugs, make sure this update is 533 # marked as security 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 # Notify security team of newly submitted security updates 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 # For command line submissions, return PackageUpdate.__str__() 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)
564 - def default(self, *args, **kw):
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): # /(testing|pending) 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: # /[testing/]<release> 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 # /pkg 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())
631 - def comment(self, text, title, karma, tg_errors=None):
632 update = PackageUpdate.byTitle(title) 633 if tg_errors: 634 flash_log(tg_errors) 635 else: 636 update.comment(text, karma) 637 raise redirect(update.get_url())
638 639 @expose(template='bodhi.templates.confirmation')
640 - def confirm_delete(self, nvr=None, ok=None, cancel=None):
641 update = PackageUpdate.byTitle(nvr) 642 if ok: 643 flash(_(u"Delete completed")) 644 raise redirect('/delete/%s' % update.title) 645 if cancel: 646 flash(_(u"Delete canceled" )) 647 raise redirect(update.get_url()) 648 return dict(form=self.ok_cancel_form, nvr=nvr)
649