bodhi/config/app.cfg | 5 +
bodhi/controllers.py | 2
bodhi/jobs.py | 25 +++++++-
bodhi/mail.py | 16 +++++
bodhi/masher.py | 15 ++++
bodhi/model.py | 29 +++++++++
bodhi/overrides.py | 119 +++++++++++++++++++++++++++++++++++++++
bodhi/templates/master.kid | 1
bodhi/templates/overrideform.kid | 43 ++++++++++++++
bodhi/templates/overrides.kid | 84 +++++++++++++++++++++++++++
bodhi/widgets.py | 13 ++++
11 files changed, 349 insertions(+), 3 deletions(-)
New commits:
commit 357f3d90db427ff69f09b6e5541a40e24dda1f0c
Author: Luke Macken <lmacken(a)redhat.com>
Date: Thu Apr 21 16:28:34 2011 -0400
Initial buildroot override support
diff --git a/bodhi/config/app.cfg b/bodhi/config/app.cfg
index 2c951f4..604aa90 100644
--- a/bodhi/config/app.cfg
+++ b/bodhi/config/app.cfg
@@ -214,6 +214,11 @@ not_yet_tested_msg = 'This update has not yet met the minimum
testing requiremen
feeds.num_days_to_show = 7
feeds.max_entries = 20
+## Buildroot Override Settings
+
+# Number of days before expiring overrides
+buildroot_overrides.expire_after = 4
+
# VIEW
# which view (template engine) to use if one is not specified in the
diff --git a/bodhi/controllers.py b/bodhi/controllers.py
index 5f954e3..fb1878f 100644
--- a/bodhi/controllers.py
+++ b/bodhi/controllers.py
@@ -51,6 +51,7 @@ from bodhi.metrics import MetricsController
from bodhi.model import (Package, PackageBuild, PackageUpdate, Release,
Bugzilla, CVE, Comment)
from bodhi.search import SearchController
+from bodhi.overrides import BuildRootOverrideController
from bodhi.widgets import CommentForm, OkCancelForm, CommentCaptchaForm
from bodhi.exceptions import (DuplicateEntryError, InvalidRequest,
PostgresIntegrityError, SQLiteIntegrityError,
@@ -65,6 +66,7 @@ class Root(controllers.RootController):
search = SearchController()
rss = Feed("rss2.0")
metrics = MetricsController()
+ override = BuildRootOverrideController()
comment_form = CommentForm()
comment_captcha_form = CommentCaptchaForm()
diff --git a/bodhi/jobs.py b/bodhi/jobs.py
index c5c2a98..4b0bbf3 100644
--- a/bodhi/jobs.py
+++ b/bodhi/jobs.py
@@ -28,7 +28,7 @@ from sqlobject.sqlbuilder import AND
from bodhi import mail
from bodhi.util import get_age_in_days
-from bodhi.model import Release, PackageUpdate
+from bodhi.model import Release, PackageUpdate, BuildRootOverride
log = logging.getLogger(__name__)
@@ -200,6 +200,19 @@ def approve_testing_updates():
log.info('approve_testing_updates job complete.')
+def expire_buildroot_overrides():
+ """ Iterate over all of the buildroot overrides, expiring
appropriately """
+ log.info('Running expire_buildroot_overrides job')
+ now = datetime.utcnow()
+ for override in BuildRootOverride.select():
+ if (now - override.expiration).days >= 0:
+ log.info('Automatically expiring buildroot override: %s' %
+ override.builds)
+ override.untag()
+ override.destroySelf()
+ log.info('expire_buildroot_overrides job complete!')
+
+
def schedule():
""" Schedule our periodic tasks """
@@ -253,3 +266,13 @@ def schedule():
interval=21600)
#weekdays=range(1,8),
#timeonday=(0,0))
+
+ # Automatically expire buildroot overrides
+ if 'approve_testing_updates' in jobs:
+ log.debug("Scheduling expire_buildroot_overrides job")
+ scheduler.add_interval_task(action=expire_buildroot_overrides,
+ # Run every 6 hours
+ initialdelay=21600,
+ interval=21600)
+ #weekdays=range(1,8),
+ #timeonday=(0,0))
diff --git a/bodhi/mail.py b/bodhi/mail.py
index f387b58..87b1405 100644
--- a/bodhi/mail.py
+++ b/bodhi/mail.py
@@ -253,6 +253,22 @@ The Critical Path update `%(package)s` has been approved.
}
},
+ 'buildroot_override' : {
+ 'body' : u"""\
+%(submitter)s has added %(package)s to the buildroot override tag for %(release)s.
+
+ Notes: %(notes)s
+
+
https://admin.fedoraproject.org/updates/overrides
+
+""",
+ 'fields' : lambda x: {
+ 'package' : x.build,
+ 'submitter' : identity.current.user_name,
+ 'release' : x.release.long_name,
+ 'notes' : x.notes,
+ }
+ },
}
fedora_errata_template = u"""\
diff --git a/bodhi/masher.py b/bodhi/masher.py
index 0340b21..3247df9 100644
--- a/bodhi/masher.py
+++ b/bodhi/masher.py
@@ -31,7 +31,7 @@ from time import sleep
from bodhi import buildsys, mail
from bodhi.util import synchronized, sanity_check_repodata
-from bodhi.model import PackageUpdate, Release
+from bodhi.model import PackageUpdate, Release, BuildRootOverride
from bodhi.metadata import ExtendedMetadata
from bodhi.exceptions import MashTaskException
@@ -473,6 +473,18 @@ class MashTask(Thread):
update.remove_tag(update.release.pending_testing_tag, koji=self.koji)
self.koji.multiCall()
+ def obsolete_buildroot_overrides(self):
+ """ Obsolete any buildroot overrides that are in this push
"""
+ for update in self.updates:
+ for build in update.builds:
+ try:
+ override = BuildRootOverride.byBuild(build.nvr)
+ log.info('Expiring buildroot override: %s' % build.nvr)
+ override.untag()
+ override.destroySelf()
+ except SQLObjectNotFound:
+ pass
+
# With a large pushes, this tends to cause much buildsystem churn, as well
# as polluting the tag history.
#def undo_move(self):
@@ -676,6 +688,7 @@ class MashTask(Thread):
# Move koji build tags
if not self.resume and len(self.updates):
self.move_builds()
+ self.obsolete_buildroot_overrides()
# Remove all pending tags
# TODO: Once AutoQA is Good To Go, then we'll want to prevent
diff --git a/bodhi/model.py b/bodhi/model.py
index 576b27f..7746e6f 100644
--- a/bodhi/model.py
+++ b/bodhi/model.py
@@ -49,7 +49,7 @@ hub = PackageHub("bodhi")
__connection__ = hub
soClasses=('Release', 'Package', 'PackageBuild',
'PackageUpdate', 'CVE',
- 'Bugzilla', 'Comment', 'User', 'Group',
'Visit')
+ 'Bugzilla', 'Comment', 'User', 'Group',
'Visit', 'BuildRootOverride')
class Release(SQLObject):
@@ -107,6 +107,10 @@ class Release(SQLObject):
return self.stable_tag + '-pending'
@property
+ def override_tag(self):
+ return '%s-override' % self.dist_tag
+
+ @property
def stable_repo(self):
id = self.name.replace('-', '').lower()
if self.name.startswith('EL'): # EPEL Hack.
@@ -1416,6 +1420,29 @@ class Bugzilla(SQLObject):
return "https://bugzilla.redhat.com/show_bug.cgi?id=%s" % self.bz_id
+class BuildRootOverride(SQLObject):
+ build = UnicodeCol(alternateID=True, notNone=True)
+ date_submitted = DateTimeCol(default=datetime.utcnow, notNone=True)
+ notes = UnicodeCol()
+ expiration = DateTimeCol(default=None)
+ date_expired = DateTimeCol(default=None)
+ submitter = UnicodeCol(notNone=True)
+ release = ForeignKey('Release')
+
+ def tag(self):
+ koji = buildsys.get_session()
+ log.debug('Tagging %s with %s' % (self.build,
+ self.release.override_tag))
+ koji.tagBuild(self.release.override_tag, self.build, force=True)
+ mail.send_admin('buildroot_override', self)
+
+ def untag(self):
+ koji = buildsys.get_session()
+ log.debug('Untagging %s with %s' % (self.build,
+ self.release.override_tag))
+ koji.untagBuild(self.release.override_tag, self.build, force=True)
+
+
class Releases(Singleton):
""" A cache of frequently used release data.
diff --git a/bodhi/overrides.py b/bodhi/overrides.py
new file mode 100644
index 0000000..cdb11c8
--- /dev/null
+++ b/bodhi/overrides.py
@@ -0,0 +1,119 @@
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; version 2 of the License.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU Library General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
+
+import logging
+
+from datetime import datetime, timedelta
+from turbogears import (expose, paginate, validate, validators, redirect,
+ error_handler, url, flash, identity, config)
+from turbogears.controllers import Controller
+
+from bodhi.model import BuildRootOverride, Release
+from bodhi.buildsys import get_session
+from bodhi.util import get_nvr, get_pkg_pushers
+from bodhi.widgets import BuildRootOverrideForm
+
+log = logging.getLogger(__name__)
+
+override_form = BuildRootOverrideForm()
+
+class BuildRootOverrideController(Controller):
+ require = identity.not_anonymous()
+
+ @expose(template="bodhi.templates.overrides")
+ @paginate('overrides', default_order='-date_submitted',
+ limit=20, max_limit=1000)
+ def index(self, build=None, tg_errors=None, *args, **kw):
+ overrides = BuildRootOverride.select()
+ return dict(overrides=overrides, title='Buildroot Overrides',
+ num_items=overrides.count())
+
+ @expose(template="bodhi.templates.form")
+ def new(self, build=None, tg_errors=None, *args, **kw):
+ if tg_errors:
+ flash(tg_errors)
+ if build:
+ log.debug('redirecting!')
+ raise redirect('/override/%s' % build)
+ return dict(form=override_form, values={}, action=url('/override/save'),
+ title='Buildroot Overrides')
+
+ @expose(allow_json=True)
+ def expire(self, build, *args, **kw):
+ """ Expire a given override """
+ override = BuildRootOverride.byBuild(build)
+ try:
+ override.untag()
+ except Exception, e:
+ log.error(str(e))
+ flash(str(e))
+ raise redirect('/override')
+ log.info('Buildroot override %s manually expired by %s' % (
+ build, identity.current.user_name))
+ flash('Buildroot override for %s successful untagged' % build)
+ override.destroySelf()
+ raise redirect('/override')
+
+ @expose(template="bodhi.templates.search")
+ @validate(form=override_form)
+ @error_handler(new)
+ def save(self, builds, notes, *args, **kw):
+ log.debug('BuildRootOverrideController.save(%s)' % builds)
+ for build in builds:
+ release = None
+ n, v, r = get_nvr(build)
+
+ # Make sure the user has commit rights
+ people, groups = get_pkg_pushers(n)
+ if identity.current.user_name not in people[0]:
+ flash("Error: You do not have commit privileges to %s" % n)
+
+ # Make sure the build is tagged correctly
+ koji = get_session()
+ tags = [tag['name'] for tag in koji.listTags(build)]
+
+ # Determine the release by the tag, and sanity check the builds
+ for tag in tags:
+ for rel in Release.select():
+ if tag == rel.candidate_tag:
+ #if release and release.name != rel.name:
+ # flash('Error: You cannot submit buildroot '
+ # 'overrides for more than one release at '
+ # 'a time')
+ # raise redirect('/override/new')
+ release = rel
+ elif tag in (rel.testing_tag, rel.stable_tag):
+ flash('Error: %s is already tagged with %s' % (
+ build, tag))
+ raise redirect('/override/new')
+
+ if not release:
+ flash('Error: Could not determine release for %s with tags %s' %
+ (builds, tags))
+ raise redirect('/override')
+
+ # Create a new overrides object
+ override = BuildRootOverride(build=build,
+ notes=notes, submitter=identity.current.user_name,
+ releaseID=release.id)
+
+ now = datetime.utcnow()
+ override.expiration = now + \
+ timedelta(days=config.get('buildroot_overrides.expire_after'))
+
+ # Tag the build
+ override.tag()
+
+ flash('Your buildroot override has been successfully tagged. '
+ 'It may take up to 20 minutes for the buildroot to regenerate.')
+ raise redirect('/override')
diff --git a/bodhi/templates/master.kid b/bodhi/templates/master.kid
index 09cee33..7133ef9 100644
--- a/bodhi/templates/master.kid
+++ b/bodhi/templates/master.kid
@@ -93,6 +93,7 @@ $(document).ready(function() {
<li><a
href="${tg.url('/')}">${tg.identity.anonymous and ' ' or
"%s's " % tg.identity.user_name}Home</a></li>
<li py:if="not tg.identity.anonymous"><a
href="${tg.url('/mine')}">My Updates
(${PackageUpdate.select(PackageUpdate.q.submitter ==
tg.identity.user_name).count()})</a></li>
<li py:if="not tg.identity.anonymous"><a
href="${tg.url('/new/')}">New Update</a></li>
+ <li py:if="not tg.identity.anonymous"><a
href="${tg.url('/override/')}">Buildroot
Overrides</a></li>
<li py:for="release in Releases().data">
<a id="${release['name']}"
href="${tg.url('/%s' %
release['name'])}">${release['long_name']}</a>
<div id="${release['name']}_releases">
diff --git a/bodhi/templates/overrideform.kid b/bodhi/templates/overrideform.kid
new file mode 100644
index 0000000..c9ee2ec
--- /dev/null
+++ b/bodhi/templates/overrideform.kid
@@ -0,0 +1,43 @@
+<form
xmlns:py="http://purl.org/kid/ns#"
+ name="${name}"
+ action="${action}"
+ method="${method}"
+ py:attrs="form_attrs" width="100%">
+
+ <h2>Buildroot Override Form</h2>
+
+ <table border="0" cellspacing="0"
cellpadding="0">
+ <tr py:for="i, field in enumerate(fields)">
+ <td class="title">
+ <label class="fieldlabel"
+ for="${field.field_id}"
+ py:content="field.label"/>
+ </td>
+ <td class="value">
+ <font color="red">
+ <span py:if="error_for(field)"
+ class="fielderror"
+ py:content="error_for(field)" />
+ </font>
+ <span py:replace="field.display(value_for(field),
**params_for(field))"/>
+ <span py:if="field.help_text"
+ class="fieldhelp"
+ py:content="field.help_text" />
+ </td>
+ </tr>
+ <tr>
+ <td class="title" />
+ <td class="value" py:content="submit.display(submit_text)"
/>
+ </tr>
+ </table>
+
+ <script type="text/javascript">
+ $(document).ready(function() {
+ $("#addField").click( function() {
+ o = $("input[@id=form_builds_text]:eq(0)");
+ o.clone().val("
"+o.val()).insertAfter(o.parent().parent()).show();
+ o.focus().val("");
+ } );
+ });
+ </script>
+</form>
diff --git a/bodhi/templates/overrides.kid b/bodhi/templates/overrides.kid
new file mode 100644
index 0000000..887d5df
--- /dev/null
+++ b/bodhi/templates/overrides.kid
@@ -0,0 +1,84 @@
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
+<html
xmlns="http://www.w3.org/1999/xhtml"
xmlns:py="http://purl.org/kid/ns#"
+ py:extends="'master.kid'">
+
+<head>
+ <meta content="text/html; charset=UTF-8"
http-equiv="content-type" py:replace="''"/>
+ <title>Fedora Updates - ${title}</title>
+</head>
+
+<body>
+ <h2>${title}</h2>
+ <a
href="${tg.url('/override/new')}"><img
src="${tg.url('/static/images/plus.png')}"/>Submit a new
override</a>
+ <br/>
+ <div py:if="num_items" class="list">
+ <span py:for="page in tg.paginate.pages">
+ <a py:if="page != tg.paginate.current_page"
+ href="${tg.paginate.get_href(page)}">${page}</a>
+ <b py:if="page ==
tg.paginate.current_page">${page}</b>
+ </span>
+ </div>
+
+ <table class="list">
+ <tr class="list">
+ <th class="list">
+ <b>Build</b>
+ </th>
+ <th class="list">
+ <b>Release</b>
+ </th>
+ <th class="list">
+ <b>Notes</b>
+ </th>
+ <th class="list">
+ <b>Submitted</b>
+ </th>
+ <th class="list">
+ <b>Submitter</b>
+ </th>
+ <th class="list">
+ <b>Expiration</b>
+ </th>
+ <th class="list">
+ <b>Manual Expiration</b>
+ </th>
+ </tr>
+ <?python row_color = "#FFFFFF" ?>
+ <tr class="list" bgcolor="${row_color}"
py:for="override in overrides">
+ <td class="list">
+ ${override.build}
+ </td>
+ <td class="list">
+ <a class="list" href="${tg.url('/%s' %
override.release.name)}">${override.release.name}</a>
+ </td>
+ <td class="list">
+ ${override.notes}
+ </td>
+ <td class="list">
+ ${override.date_submitted}
+ </td>
+ <td class="list">
+ <a href="${tg.url('/user/' +
override.submitter)}">${override.submitter}</a>
+ </td>
+ <td class="list">
+ ${override.expiration}
+ </td>
+ <td class="list">
+ <div py:if="'releng' in tg.identity.groups or
tg.identity.user_name == override.submitter">
+ <a href="${tg.url('/override/expire/%s' %
override.build)}">Expire</a>
+ </div>
+ </td>
+ <?python row_color = (row_color == "#f1f1f1") and
"#FFFFFF" or "#f1f1f1" ?>
+ </tr>
+ </table>
+
+ <div py:if="num_items" class="list">
+ <span py:for="page in tg.paginate.pages">
+ <a py:if="page != tg.paginate.current_page"
+ href="${tg.paginate.get_href(page)}">${page}</a>
+ <b py:if="page ==
tg.paginate.current_page">${page}</b>
+ </span>
+ </div>
+
+</body>
+</html>
diff --git a/bodhi/widgets.py b/bodhi/widgets.py
index eb9c70e..3f294f1 100644
--- a/bodhi/widgets.py
+++ b/bodhi/widgets.py
@@ -192,3 +192,16 @@ class SortableDataGrid(DataGrid):
javascript = [LocalJSLink('bodhi', '/static/js/jquery.js'),
LocalJSLink('bodhi',
'/static/js/jquery.tablesorter.js')]
css = [LocalCSSLink('bodhi', '/static/css/flora.tablesorter.css')]
+
+class BuildRootOverrideForm(Form):
+ template = "bodhi.templates.overrideform"
+ fields = [
+ AutoCompleteField('builds', label='Build',
+ search_controller=url('/new/search'),
+ search_param='name', result_name='pkgs',
+ template='bodhi.templates.packagefield',
+ validator=AutoCompleteValidator()),
+ TextArea('notes', validator=validators.UnicodeString(),
+ rows=13, cols=65),
+ ]
+ submit_text = "Submit"