diff --git a/bodhi/client/__init__.py b/bodhi/client/__init__.py
index 497881b07d..6e95218ff9 100644
--- a/bodhi/client/__init__.py
+++ b/bodhi/client/__init__.py
@@ -59,7 +59,7 @@ def _warn_if_url_and_staging_set(ctx, param, value):
click.option('--bugs', help='Comma-separated list of bug numbers', default=''),
click.option('--close-bugs', default=True, is_flag=True, help='Automatically close bugs'),
click.option('--request', help='Requested repository',
- type=click.Choice(['testing', 'stable', 'unpush'])),
+ type=click.Choice(['testing', 'stable', 'unpush', 'batched'])),
click.option('--autokarma', is_flag=True, help='Enable karma automatism'),
click.option('--stable-karma', type=click.INT, help='Stable karma threshold'),
click.option('--unstable-karma', type=click.INT, help='Unstable karma threshold'),
@@ -274,7 +274,7 @@ def edit(user, password, url, **kwargs):
@click.option('--releases', help='Updates for specific releases')
@click.option('--locked', help='Updates that are in a locked state')
@click.option('--request', help='Updates with a specific request',
- type=click.Choice(['testing', 'stable', 'unpush']))
+ type=click.Choice(['testing', 'stable', 'unpush', 'batched']))
@click.option('--submitted-since',
help='Updates that have been submitted since a certain time')
@click.option('--status', help='Filter by update status',
@@ -324,7 +324,7 @@ def request(update, state, user, password, url, **kwargs):
UPDATE: The title of the update (e.g. FEDORA-2017-f8e0ef2850)
STATE: The state you wish to change the update\'s request to. Valid options are
- testing, stable, obsolete, unpush, and revoke.
+ testing, stable, obsolete, unpush, batched, and revoke.
"""
# Developer Docs
diff --git a/bodhi/server/config.py b/bodhi/server/config.py
index 385ba0352c..41a1b29a04 100644
--- a/bodhi/server/config.py
+++ b/bodhi/server/config.py
@@ -510,6 +510,9 @@ class BodhiConfig(dict):
'value': ('%s has been pushed to the %s repository. If problems still persist, please '
'make note of it in this bug report.'),
'validator': unicode},
+ 'stable_from_batched_msg': {
+ 'value': ('This update has been dequeued from batched and is now entering stable.'),
+ 'validator': unicode},
'stacks_enabled': {
'value': False,
'validator': _validate_bool},
diff --git a/bodhi/server/models.py b/bodhi/server/models.py
index ab99deed08..c4f2f50a56 100644
--- a/bodhi/server/models.py
+++ b/bodhi/server/models.py
@@ -1731,8 +1731,8 @@ def set_request(self, db, action, username):
# If status is testing going to stable request and action is revoke,
# keep the status at testing
- elif self.status is UpdateStatus.testing and self.request is UpdateRequest.stable \
- and action is UpdateRequest.revoke:
+ elif self.request in (UpdateRequest.stable, UpdateRequest.batched) and \
+ self.status is UpdateStatus.testing and action is UpdateRequest.revoke:
self.status = UpdateStatus.testing
self.revoke()
flash_log("%s has been revoked." % self.title)
@@ -1748,7 +1748,7 @@ def set_request(self, db, action, username):
return
# Disable pushing critical path updates for pending releases directly to stable
- if action is UpdateRequest.stable and self.critpath:
+ if action in (UpdateRequest.stable, UpdateRequest.batched) and self.critpath:
if config.get('critpath.num_admin_approvals') is not None:
if not self.critpath_approved:
stern_note = (
@@ -1774,7 +1774,7 @@ def set_request(self, db, action, username):
# Ensure this update meets the minimum testing requirements
flash_notes = ''
- if action is UpdateRequest.stable and not self.critpath:
+ if action in (UpdateRequest.stable, UpdateRequest.batched) and not self.critpath:
# Check if we've met the karma requirements
if (self.stable_karma not in (None, 0) and self.karma >=
self.stable_karma) or self.critpath_approved:
@@ -2373,9 +2373,14 @@ def check_karma_thresholds(self, db, agent):
self.comment(db, text, author=u'bodhi')
elif self.stable_karma and self.karma >= self.stable_karma:
if self.autokarma:
- log.info("Automatically marking %s as stable" % self.title)
- self.set_request(db, UpdateRequest.stable, agent)
- self.request = UpdateRequest.stable
+ if self.severity is UpdateSeverity.urgent or self.type is UpdateType.newpackage:
+ log.info("Automatically marking %s as stable" % self.title)
+ self.set_request(db, UpdateRequest.stable, agent)
+ else:
+ log.info("Automatically adding %s to batch of updates that will be pushed to"
+ " stable at a later date" % self.title)
+ self.set_request(db, UpdateRequest.batched, agent)
+
self.date_pushed = None
notifications.publish(
topic='update.karma.threshold.reach',
@@ -2571,7 +2576,7 @@ def requested_tag(self):
# release to the Release.dist-tag
if self.release.state is ReleaseState.pending:
tag = self.release.dist_tag
- elif self.request is UpdateRequest.testing:
+ elif self.request in (UpdateRequest.testing, UpdateRequest.batched):
tag = self.release.testing_tag
elif self.request is UpdateRequest.obsolete:
tag = self.release.candidate_tag
diff --git a/bodhi/server/scripts/dequeue_stable.py b/bodhi/server/scripts/dequeue_stable.py
new file mode 100644
index 0000000000..4ca7d0036e
--- /dev/null
+++ b/bodhi/server/scripts/dequeue_stable.py
@@ -0,0 +1,46 @@
+# -*- coding: utf-8 -*-
+# Copyright © 2017 Caleigh Runge-Hottman
+#
+# This file is part of Bodhi.
+#
+# 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; either version 2
+# of the License, or (at your option) any later version.
+#
+# 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 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., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+"""This script is responsible for moving all updates with a batched request to a stable request."""
+
+import sys
+
+import click
+
+from bodhi.server import buildsys, config, models, Session, initialize_db
+
+
+@click.command()
+@click.version_option(message='%(version)s')
+def dequeue_stable():
+ """Convert all batched requests to stable requests."""
+ initialize_db(config.config)
+ buildsys.setup_buildsystem(config.config)
+ db = Session()
+
+ try:
+ batched = db.query(models.Update).filter_by(request=models.UpdateRequest.batched).all()
+ for update in batched:
+ update.set_request(db, models.UpdateRequest.stable, u'bodhi')
+ db.commit()
+
+ except Exception as e:
+ print(str(e))
+ db.rollback()
+ Session.remove()
+ sys.exit(1)
diff --git a/bodhi/server/services/updates.py b/bodhi/server/services/updates.py
index a8b3f3830b..7f3de01931 100644
--- a/bodhi/server/services/updates.py
+++ b/bodhi/server/services/updates.py
@@ -137,7 +137,7 @@ def set_request(request):
"Can't change request for an archived release")
return
- if action is UpdateRequest.stable:
+ if action in (UpdateRequest.stable, UpdateRequest.batched):
settings = request.registry.settings
result, reason = update.check_requirements(request.db, settings)
if not result:
diff --git a/bodhi/server/templates/update.html b/bodhi/server/templates/update.html
index ec0a03f030..dcbef2779f 100644
--- a/bodhi/server/templates/update.html
+++ b/bodhi/server/templates/update.html
@@ -273,7 +273,7 @@
$("#updatebuttons #edit").attr('href',
'${request.route_url("update_edit", id=update.alias)}');
- $.each(['testing', 'stable', 'unpush', 'revoke'], function(i, action) {
+ $.each(['testing', 'stable', 'batched', 'unpush', 'revoke'], function(i, action) {
$("#updatebuttons #" + action).click(function() {
$("#updatebuttons a").addClass('disabled');
cabbage.spin(); // for real!
@@ -411,8 +411,10 @@
Push to Testing
% endif
% if update.status.description not in ['stable', 'obsolete']:
- Push to Stable
+ Push to Batched
% endif
+ % elif update.request.description == 'batched':
+ Push to Stable
% else:
Revoke
% endif
diff --git a/bodhi/server/util.py b/bodhi/server/util.py
index e675c6c3e5..9125654755 100644
--- a/bodhi/server/util.py
+++ b/bodhi/server/util.py
@@ -468,6 +468,7 @@ def request2html(context, request):
'obsolete': 'default',
'testing': 'warning',
'stable': 'success',
+ 'batched': 'success',
}.get(request)
return "%s" % (cls, request)
diff --git a/bodhi/server/validators.py b/bodhi/server/validators.py
index e7e009a0f5..f1ed08d7aa 100644
--- a/bodhi/server/validators.py
+++ b/bodhi/server/validators.py
@@ -1034,7 +1034,7 @@ def validate_request(request):
if 'request' not in request.validated:
# Invalid request. Let the colander error from our schemas.py bubble up.
return
- if request.validated['request'] is UpdateRequest.stable:
+ if request.validated['request'] in (UpdateRequest.stable, UpdateRequest.batched):
target = UpdateStatus.stable
elif request.validated['request'] is UpdateRequest.testing:
target = UpdateStatus.testing
diff --git a/bodhi/tests/server/consumers/test_masher.py b/bodhi/tests/server/consumers/test_masher.py
index a8d294eca3..9e897dad01 100644
--- a/bodhi/tests/server/consumers/test_masher.py
+++ b/bodhi/tests/server/consumers/test_masher.py
@@ -1053,7 +1053,7 @@ def test_stable_requirements_met_during_push(self, *args):
# Ensure the masher set the autokarma once the push is done
self.assertEquals(up.locked, False)
- self.assertEquals(up.request, UpdateRequest.stable)
+ self.assertEquals(up.request, UpdateRequest.batched)
@mock.patch(**mock_taskotron_results)
@mock.patch('bodhi.server.consumers.masher.MasherThread.update_comps')
diff --git a/bodhi/tests/server/scripts/test_dequeue_stable.py b/bodhi/tests/server/scripts/test_dequeue_stable.py
new file mode 100644
index 0000000000..267d09e0cf
--- /dev/null
+++ b/bodhi/tests/server/scripts/test_dequeue_stable.py
@@ -0,0 +1,65 @@
+# -*- coding: utf-8 -*-
+# Copyright © 2017 Caleigh Runge-Hottman
+#
+# This file is part of Bodhi.
+#
+# 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; either version 2
+# of the License, or (at your option) any later version.
+#
+# 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 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., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+"""
+This module contains tests for the bodhi.server.scripts.dequeue_stable module.
+"""
+from datetime import datetime, timedelta
+
+from click import testing
+
+from bodhi.server import models
+from bodhi.server.scripts import dequeue_stable
+from bodhi.tests.server.base import BaseTestCase
+
+
+class TestDequeueStable(BaseTestCase):
+ """
+ This class contains tests for the dequeue_stable() function.
+ """
+ def test_dequeue_stable(self):
+ """
+ Assert that dequeue_stable moves only the batched updates to stable.
+ """
+ runner = testing.CliRunner()
+
+ update = self.db.query(models.Update).all()[0]
+ update.request = models.UpdateRequest.batched
+ update.locked = False
+ update.date_testing = datetime.utcnow() - timedelta(days=7)
+ self.db.commit()
+
+ result = runner.invoke(dequeue_stable.dequeue_stable, [])
+ self.assertEqual(result.exit_code, 0)
+
+ update = self.db.query(models.Update).all()[0]
+ self.assertEqual(update.request, models.UpdateRequest.stable)
+
+ def test_dequeue_stable_exception(self):
+ """
+ Assert that a locked update triggers an exception, and doesn't move to stable.
+ """
+ runner = testing.CliRunner()
+ update = self.db.query(models.Update).all()[0]
+ update.request = models.UpdateRequest.batched
+ self.db.commit()
+
+ result = runner.invoke(dequeue_stable.dequeue_stable, [])
+
+ self.assertEqual(result.exit_code, 1)
+ self.assertEqual(result.output, u"Can't change the request on a locked update\n")
diff --git a/bodhi/tests/server/services/test_updates.py b/bodhi/tests/server/services/test_updates.py
index 61de44ee6b..712b90ae04 100644
--- a/bodhi/tests/server/services/test_updates.py
+++ b/bodhi/tests/server/services/test_updates.py
@@ -29,7 +29,7 @@
from bodhi.server.models import (
BuildrootOverride, Group, RpmPackage, Release,
ReleaseState, RpmBuild, Update, UpdateRequest, UpdateStatus, UpdateType,
- User, CiStatus)
+ UpdateSeverity, User, CiStatus)
from bodhi.tests.server import base
@@ -660,7 +660,7 @@ def test_provenpackager_request_privs(self, publish, *args):
update.comment(self.db, u"foo", 1, u'biz')
update = self.db.query(Update).filter_by(title=nvr).one()
self.assertEqual(update.karma, 3)
- self.assertEqual(update.request, UpdateRequest.stable)
+ self.assertEqual(update.request, UpdateRequest.batched)
# Set it back to testing
update.request = UpdateRequest.testing
@@ -2234,7 +2234,7 @@ def test_pending_update_on_stable_karma_reached_autopush_enabled(self, publish,
up = self.db.query(Update).filter_by(title=nvr).one()
self.assertEquals(up.karma, 2)
- self.assertEquals(up.request, UpdateRequest.stable)
+ self.assertEquals(up.request, UpdateRequest.batched)
self.assertEquals(up.status, UpdateStatus.pending)
@mock.patch(**mock_valid_requirements)
@@ -3455,7 +3455,7 @@ def test_autopush_critical_update_with_no_negative_karma(self, publish, *args):
self.assertEquals(up.autokarma, True)
up = self.db.query(Update).filter_by(title=resp.json['title']).one()
- self.assertEquals(up.request, UpdateRequest.stable)
+ self.assertEquals(up.request, UpdateRequest.batched)
@mock.patch(**mock_valid_requirements)
@mock.patch('bodhi.server.notifications.publish')
@@ -3614,7 +3614,7 @@ def test_autopush_non_critical_update_with_no_negative_karma(self, publish, *arg
"""
Make sure autopush doesn't get disabled for Non Critical update if it
does not receive any negative karma. Test update gets automatically
- marked as stable.
+ marked as batched.
"""
user = User(name=u'bob')
self.db.add(user)
@@ -3644,7 +3644,7 @@ def test_autopush_non_critical_update_with_no_negative_karma(self, publish, *arg
self.assertEquals(up.autokarma, True)
up = self.db.query(Update).filter_by(title=resp.json['title']).one()
- self.assertEquals(up.request, UpdateRequest.stable)
+ self.assertEquals(up.request, UpdateRequest.batched)
@mock.patch(**mock_valid_requirements)
@mock.patch('bodhi.server.notifications.publish')
@@ -3666,6 +3666,53 @@ def test_manually_push_to_stable_based_on_karma(self, publish, *args):
resp = self.app.post_json('/updates/', args)
publish.assert_called_with(topic='update.request.testing', msg=ANY)
+ # Marks it as batched
+ upd = Update.get(nvr, self.db)
+ upd.status = UpdateStatus.testing
+ upd.request = UpdateRequest.batched
+ upd.date_testing = datetime.now() - timedelta(days=1)
+ self.db.commit()
+
+ # Checks karma threshold is reached
+ # Makes sure stable karma is not None
+ # Ensures Request doesn't get set to stable automatically since autokarma is disabled
+ upd.comment(self.db, u'LGTM', author=u'ralph', karma=1)
+ upd = Update.get(nvr, self.db)
+ self.assertEquals(upd.karma, 1)
+ self.assertEquals(upd.stable_karma, 1)
+ self.assertEquals(upd.status, UpdateStatus.testing)
+ self.assertEquals(upd.request, UpdateRequest.batched)
+ self.assertEquals(upd.autokarma, False)
+
+ text = unicode(config.get('testing_approval_msg_based_on_karma'))
+ upd.comment(self.db, text, author=u'bodhi')
+
+ # Checks Push to Stable text in the html page for this update
+ id = 'bodhi-2.0.0-2.fc17'
+ resp = self.app.get('/updates/%s' % id,
+ headers={'Accept': 'text/html'})
+ self.assertIn('text/html', resp.headers['Content-Type'])
+ self.assertIn(id, resp)
+ self.assertIn('Push to Stable', resp)
+
+ @mock.patch(**mock_valid_requirements)
+ @mock.patch('bodhi.server.notifications.publish')
+ def test_manually_push_to_batched_based_on_karma(self, publish, *args):
+ """
+ Test manually push to batched when autokarma is disabled
+ and karma threshold is reached. Ensure that the option/button to push to
+ stable is not present prior to entering the batched request state.
+ """
+
+ # Disabled
+ # Sets stable karma to 1
+ nvr = u'bodhi-2.0.0-2.fc17'
+ args = self.get_update(nvr)
+ args['autokarma'] = False
+ args['stable_karma'] = 1
+ resp = self.app.post_json('/updates/', args)
+ publish.assert_called_with(topic='update.request.testing', msg=ANY)
+
# Marks it as testing
upd = Update.get(nvr, self.db)
upd.status = UpdateStatus.testing
@@ -3687,13 +3734,14 @@ def test_manually_push_to_stable_based_on_karma(self, publish, *args):
text = unicode(config.get('testing_approval_msg_based_on_karma'))
upd.comment(self.db, text, author=u'bodhi')
- # Checks Push to Stable text in the html page for this update
+ # Checks Push to Batched text in the html page for this update
id = 'bodhi-2.0.0-2.fc17'
resp = self.app.get('/updates/%s' % id,
headers={'Accept': 'text/html'})
self.assertIn('text/html', resp.headers['Content-Type'])
self.assertIn(id, resp)
- self.assertIn('Push to Stable', resp)
+ self.assertIn('Push to Batched', resp)
+ self.assertNotIn('Push to Stable', resp)
@mock.patch(**mock_valid_requirements)
@mock.patch('bodhi.server.notifications.publish')
@@ -3831,10 +3879,74 @@ def test_batched_update(self, publish, *args):
resp = self.app.post_json('/updates/', args)
up = self.db.query(Update).filter_by(title=resp.json['title']).one()
up.builds[0].ci_status = CiStatus.passed
+ up.comment(self.db, u"foo1", 1, u'foo1')
+ up.comment(self.db, u"foo2", 1, u'foo2')
self.db.commit()
+
resp = self.app.post_json(
'/updates/%s/request' % args['builds'],
{'request': 'batched', 'csrf_token': self.get_csrf_token()})
+
self.assertEqual(resp.json['update']['request'], 'batched')
publish.assert_called_with(
topic='update.request.batched', msg=mock.ANY)
+
+ @mock.patch(**mock_valid_requirements)
+ @mock.patch('bodhi.server.notifications.publish')
+ def test_newpackage_update_bypass_batched(self, publish, *args):
+ """
+ Make sure a security update skips the 'batched' request and immediately enters stable
+ upon getting the sufficient number of karma.
+ """
+ nvr = u'bodhi-2.0.0-2.fc17'
+ args = self.get_update(nvr)
+ args['autokarma'] = True
+ args['stable_karma'] = 2
+
+ resp = self.app.post_json('/updates/', args)
+ self.assertEquals(resp.json['request'], 'testing')
+ publish.assert_called_with(topic='update.request.testing', msg=ANY)
+
+ up = self.db.query(Update).filter_by(title=resp.json['title']).one()
+ up.status = UpdateStatus.testing
+ up.type = UpdateType.newpackage
+ self.db.commit()
+
+ up.comment(self.db, u'cool beans', author=u'mrgroovy', karma=1)
+ up = self.db.query(Update).filter_by(title=resp.json['title']).one()
+
+ up.comment(self.db, u'lgtm', author=u'caleigh', karma=1)
+ up = self.db.query(Update).filter_by(title=resp.json['title']).one()
+
+ up = self.db.query(Update).filter_by(title=resp.json['title']).one()
+ self.assertEquals(up.request, UpdateRequest.stable)
+
+ @mock.patch(**mock_valid_requirements)
+ @mock.patch('bodhi.server.notifications.publish')
+ def test_urgent_update_bypass_batched(self, publish, *args):
+ """
+ Make sure an urgent update skips the 'batched' request and immediately enters stable
+ upon getting the sufficient number of karma.
+ """
+ nvr = u'bodhi-2.0.0-2.fc17'
+ args = self.get_update(nvr)
+ args['autokarma'] = True
+ args['stable_karma'] = 2
+
+ resp = self.app.post_json('/updates/', args)
+ self.assertEquals(resp.json['request'], 'testing')
+ publish.assert_called_with(topic='update.request.testing', msg=ANY)
+
+ up = self.db.query(Update).filter_by(title=resp.json['title']).one()
+ up.status = UpdateStatus.testing
+ up.severity = UpdateSeverity.urgent
+ self.db.commit()
+
+ up.comment(self.db, u'cool beans', author=u'mrgroovy', karma=1)
+ up = self.db.query(Update).filter_by(title=resp.json['title']).one()
+
+ up.comment(self.db, u'lgtm', author=u'caleigh', karma=1)
+ up = self.db.query(Update).filter_by(title=resp.json['title']).one()
+
+ up = self.db.query(Update).filter_by(title=resp.json['title']).one()
+ self.assertEquals(up.request, UpdateRequest.stable)
diff --git a/bodhi/tests/server/test_models.py b/bodhi/tests/server/test_models.py
index 61943f9b8d..7b65cedf67 100644
--- a/bodhi/tests/server/test_models.py
+++ b/bodhi/tests/server/test_models.py
@@ -1313,7 +1313,7 @@ def test_stable_karma(self, publish):
self.assertEqual(update.request, None)
update.comment(self.db, u"foo", 1, u'biz')
self.assertEqual(update.karma, 3)
- self.assertEqual(update.request, UpdateRequest.stable)
+ self.assertEqual(update.request, UpdateRequest.batched)
publish.assert_called_with(topic='update.comment', msg=mock.ANY)
@mock.patch('bodhi.server.notifications.publish')
diff --git a/setup.py b/setup.py
index ba739ed81a..be5180a810 100644
--- a/setup.py
+++ b/setup.py
@@ -154,6 +154,7 @@ def get_requirements(requirements_file='requirements.txt'):
initialize_bodhi_db = bodhi.server.scripts.initializedb:main
bodhi-babysit-ci = bodhi.server.scripts.babysit_ci:babysit
bodhi-clean-old-mashes = bodhi.server.scripts.clean_old_mashes:clean_up
+ bodhi-dequeue-stable = bodhi.server.scripts.dequeue_stable:dequeue_stable
bodhi-push = bodhi.server.push:push
bodhi-expire-overrides = bodhi.server.scripts.expire_overrides:main
bodhi-untag-branched = bodhi.server.scripts.untag_branched:main