From 79a7da7c0bb57b006b15b28299b9ea0ca662c582 Mon Sep 17 00:00:00 2001 From: Tomas Kopecek Date: Oct 16 2019 08:03:46 +0000 Subject: CGUninitBuild for cancelling CG reservations Fixes: https://pagure.io/koji/issue/1610 --- diff --git a/hub/kojihub.py b/hub/kojihub.py index 9c7e471..7e16b87 100644 --- a/hub/kojihub.py +++ b/hub/kojihub.py @@ -5816,6 +5816,43 @@ def cg_init_build(cg, data): return {'build_id': build_id, 'token': token} +def cg_refund_build(cg, build_id, token, state=koji.BUILD_STATES['FAILED']): + """If build is reserved and not finished yet, there is an option + to release reservation and mark build either FAILED or CANCELED. + For this calling CG needs to know build_id and reservation token. + + On success it doesn't return nothing. On error it raises an exception. + """ + + if state not in (koji.BUILD_STATES['FAILED'], koji.BUILD_STATES['CANCELED']): + raise koji.GenericError("Only FAILED/CANCELLED build states are allowed") + + assert_cg(cg) + binfo = get_build(build_id, strict=True) + if binfo['state'] != koji.BUILD_STATES['BUILDING']: + raise koji.GenericError('Build ID %s is not in BUILDING state' % build_id) + + build_token = get_reservation_token(build_id) + if not build_token or build_token['token'] != token: + raise koji.GenericError("Token doesn't match build ID %s" % build_id) + + cg_id = lookup_name('content_generator', cg, strict=True)['id'] + if binfo['cg_id'] != cg_id: + raise koji.GenericError('Build ID %s is not reserved by this CG' % build_id) + + koji.plugin.run_callbacks('preBuildStateChange', attribute='state', + old=koji.BUILD_STATES['BUILDING'], new=state, info=binfo) + + update = UpdateProcessor('build', values={'id': build_id}, clauses=["id = %(id)s"]) + update.set(state=state) + update.rawset(completion_time='NOW()') + update.execute() + + binfo = get_build(build_id, strict=True) + koji.plugin.run_callbacks('postBuildStateChange', attribute='state', + old=koji.BUILD_STATES['BUILDING'], new=state, info=binfo) + + def cg_import(metadata, directory, token=None): """Import build from a content generator @@ -9939,6 +9976,7 @@ class RootExports(object): import_archive(fullpath, buildinfo, type, typeInfo) CGInitBuild = staticmethod(cg_init_build) + CGRefundBuild = staticmethod(cg_refund_build) CGImport = staticmethod(cg_import) untaggedBuilds = staticmethod(untagged_builds) diff --git a/tests/test_hub/test_cg_importer.py b/tests/test_hub/test_cg_importer.py index 032658c..defe710 100644 --- a/tests/test_hub/test_cg_importer.py +++ b/tests/test_hub/test_cg_importer.py @@ -11,6 +11,8 @@ import koji import kojihub from koji import GenericError +IP = kojihub.InsertProcessor +UP = kojihub.UpdateProcessor class TestCGImporter(unittest.TestCase): TMP_PATH = os.path.join(os.path.dirname(__file__), 'tmptest') @@ -224,3 +226,134 @@ class TestMatchKojiFile(unittest.TestCase): self.get_build.return_value = self.build1 with self.assertRaises(koji.GenericError): self.importer.match_kojifile(comp) + + +class TestCGReservation(unittest.TestCase): + def getInsert(self, *args, **kwargs): + insert = IP(*args, **kwargs) + insert.execute = mock.MagicMock() + self.inserts.append(insert) + return insert + + def getUpdate(self, *args, **kwargs): + update = UP(*args, **kwargs) + update.execute = mock.MagicMock() + self.updates.append(update) + return update + + + def setUp(self): + self.InsertProcessor = mock.patch('kojihub.InsertProcessor', + side_effect=self.getInsert).start() + self.UpdateProcessor = mock.patch('kojihub.UpdateProcessor', + side_effect=self.getUpdate).start() + self.inserts = [] + self.updates = [] + + self.context = mock.patch('kojihub.context').start() + self.context.session.user_id = 123456 + self.mock_cursor = mock.MagicMock() + self.context.cnx.cursor.return_value = self.mock_cursor + + def tearDown(self): + mock.patch.stopall() + + @mock.patch("kojihub.new_build") + @mock.patch("kojihub.get_user") + @mock.patch("kojihub.lookup_name") + @mock.patch("kojihub.assert_cg") + def test_init_build_ok(self, assert_cg, lookup_name, get_user, new_build): + assert_cg.return_value = True + lookup_name.return_value = {'id': 21, 'name': 'cg_name'} + get_user.return_value = {'id': 123456, 'name': 'username'} + new_build.return_value = 654 + cg = 'content_generator_name' + self.mock_cursor.fetchone.side_effect = [ + [333], # get pkg_id + [1234], # get nextval pkg_id + ] + self.mock_cursor.fetchall.side_effect = [ + [[]], + ] + + data = { + 'name': 'pkg_name', + 'version': 'pkg_version', + 'release': 'pkg_release', + 'extra': {}, + } + + kojihub.cg_init_build(cg, data) + + lookup_name.assert_called_once_with('content_generator', cg, strict=True) + assert_cg.assert_called_once_with(cg) + self.assertEqual(1, len(self.inserts)) + insert = self.inserts[0] + self.assertEqual(insert.table, 'build_reservations') + self.assertEqual(insert.data['build_id'], 654) + self.assertTrue('token' in insert.data) + self.assertEqual(insert.rawdata, {'created': 'NOW()'}) + + @mock.patch("koji.plugin.run_callbacks") + @mock.patch("kojihub.get_reservation_token") + @mock.patch("kojihub.lookup_name") + @mock.patch("kojihub.get_build") + @mock.patch("kojihub.assert_cg") + def test_uninit_build_ok(self, assert_cg, get_build, lookup_name, get_reservation_token, + run_callbacks): + assert_cg.return_value = True + build_id = 1122 + cg_id = 888 + cg = 'content_generator_name' + get_build.side_effect = [ + { + 'id': build_id, + 'state': koji.BUILD_STATES['BUILDING'], + 'cg_id': cg_id, + }, + { + 'id': build_id, + 'state': koji.BUILD_STATES['FAILED'], + 'cg_id': cg_id, + }, + ] + + token = 'random_token' + get_reservation_token.return_value = {'build_id': build_id, 'token': token} + lookup_name.return_value = {'name': cg, 'id': cg_id} + + kojihub.cg_refund_build(cg, build_id, token) + + assert_cg.assert_called_once_with(cg) + get_build.assert_has_calls([ + mock.call(build_id, strict=True), + mock.call(build_id, strict=True), + ]) + get_reservation_token.assert_called_once_with(build_id) + lookup_name.assert_called_once_with('content_generator', cg, strict=True) + + self.assertEqual(len(self.updates), 1) + update = self.updates[0] + self.assertEqual(update.table, 'build') + self.assertEqual(update.values['id'], build_id) + self.assertEqual(update.data['state'], koji.BUILD_STATES['FAILED']) + self.assertEqual(update.rawdata, {'completion_time': 'NOW()'}) + + run_callbacks.assert_has_calls([ + mock.call('preBuildStateChange', attribute='state', + old=koji.BUILD_STATES['BUILDING'], + new=koji.BUILD_STATES['FAILED'], + info={ + 'state': koji.BUILD_STATES['BUILDING'], + 'cg_id': cg_id, + 'id': build_id} + ), + mock.call('postBuildStateChange', attribute='state', + old=koji.BUILD_STATES['BUILDING'], + new=koji.BUILD_STATES['FAILED'], + info={ + 'state': koji.BUILD_STATES['FAILED'], + 'cg_id': cg_id, + 'id': build_id} + ), + ])