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..c0e19dbfc0 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.security: + 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..70d1ecdd5a --- /dev/null +++ b/bodhi/server/scripts/dequeue_stable.py @@ -0,0 +1,45 @@ +# -*- 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 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) + 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/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..d07b20147a 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') @@ -3830,11 +3830,75 @@ def test_batched_update(self, publish, *args): args = self.get_update('bodhi-2.0.0-3.fc17') 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.app_settings['ci.required'] = True 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_security_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.security + 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)