From b1f4ed451b51fc65b9afff05cf2bc5fa7e66d2a2 Mon Sep 17 00:00:00 2001 From: Jana Cupova Date: Mar 22 2023 04:44:06 +0000 Subject: Add repoID in listBuildroots and create repoinfo command Add repoID param in listBuildroots and show result in repoInfo Create new command repoinfo which provides equivalent info as repoInfo Add number of buildroots related to repo in repoinfo page Fixes: https://pagure.io/koji/issue/2549 --- diff --git a/cli/koji_cli/commands.py b/cli/koji_cli/commands.py index 72ea3a2..725bb97 100644 --- a/cli/koji_cli/commands.py +++ b/cli/koji_cli/commands.py @@ -7741,3 +7741,56 @@ def anon_handle_userinfo(goptions, session, args): print("Number of tasks: %d" % tasks.result) print("Number of builds: %d" % builds.result) print('') + + +def anon_handle_repoinfo(goptions, session, args): + "[info] Print basic information about a repo" + usage = "usage: %prog repoinfo [options] [ ...]" + parser = OptionParser(usage=get_usage_str(usage)) + parser.add_option("--buildroots", action="store_true", + help="Prints list of buildroot IDs") + (options, args) = parser.parse_args(args) + if len(args) < 1: + parser.error("Please specify a repo ID") + ensure_connection(session, goptions) + + kojipath = koji.PathInfo(topdir=goptions.topurl) + + with session.multicall() as m: + result = [m.repoInfo(repo_id, strict=False) for repo_id in args] + + for repo_id, repoinfo in zip(args, result): + rinfo = repoinfo.result + if not rinfo: + warn("No such repo: %s\n" % repo_id) + continue + print('ID: %s' % rinfo['id']) + print('Tag ID: %s' % rinfo['tag_id']) + print('Tag name: %s' % rinfo['tag_name']) + print('State: %s' % koji.REPO_STATES[rinfo['state']]) + print("Created: %s" % koji.formatTimeLong(rinfo['create_ts'])) + print('Created event: %s' % rinfo['create_event']) + url = kojipath.repo(rinfo['id'], rinfo['tag_name']) + print('URL: %s' % url) + if rinfo['dist']: + repo_json = os.path.join( + kojipath.distrepo(rinfo['id'], rinfo['tag_name']), 'repo.json') + else: + repo_json = os.path.join( + kojipath.repo(rinfo['id'], rinfo['tag_name']), 'repo.json') + print('Repo json: %s' % repo_json) + print("Dist repo?: %s" % (rinfo['dist'] and 'yes' or 'no')) + print('Task ID: %s' % rinfo['task_id']) + try: + repo_buildroots = session.listBuildroots(repoID=rinfo['id']) + count_buildroots = len(repo_buildroots) + print('Number of buildroots: %i' % count_buildroots) + if options.buildroots and count_buildroots > 0: + repo_buildroots_id = [repo_buildroot['id'] for repo_buildroot in repo_buildroots] + print('Buildroots ID:') + for r_bldr_id in repo_buildroots_id: + print(' ' * 15 + '%s' % r_bldr_id) + except koji.ParameterError: + # repoID option added in 1.33 + if options.buildroots: + warn("--buildroots option is available with hub 1.33 or newer") diff --git a/kojihub/kojihub.py b/kojihub/kojihub.py index f705837..9c86a8c 100644 --- a/kojihub/kojihub.py +++ b/kojihub/kojihub.py @@ -5583,7 +5583,7 @@ def get_channel(channelInfo, strict=False): def query_buildroots(hostID=None, tagID=None, state=None, rpmID=None, archiveID=None, taskID=None, - buildrootID=None, queryOpts=None): + buildrootID=None, repoID=None, queryOpts=None): """Return a list of matching buildroots Optional args: @@ -5685,6 +5685,18 @@ def query_buildroots(hostID=None, tagID=None, state=None, rpmID=None, archiveID= if not candidate_buildroot_ids: return _applyQueryOpts([], queryOpts) + if repoID: + query = QueryProcessor(columns=['buildroot_id'], tables=['standard_buildroot'], + clauses=['repo_id = %(repoID)i'], opts={'asList': True}, + values=locals()) + result = set(query.execute()) + if candidate_buildroot_ids: + candidate_buildroot_ids &= result + else: + candidate_buildroot_ids = result + if not candidate_buildroot_ids: + return _applyQueryOpts([], queryOpts) + if candidate_buildroot_ids: candidate_buildroot_ids = list(candidate_buildroot_ids) clauses.append('buildroot.id IN %(candidate_buildroot_ids)s') diff --git a/tests/test_cli/data/list-commands.txt b/tests/test_cli/data/list-commands.txt index bdcd739..c434b68 100644 --- a/tests/test_cli/data/list-commands.txt +++ b/tests/test_cli/data/list-commands.txt @@ -117,6 +117,7 @@ info commands: list-untagged List untagged builds list-volumes List storage volumes mock-config Create a mock config + repoinfo Print basic information about a repo rpminfo Print basic information about an RPM show-groups Show groups data for a tag taginfo Print basic information about a tag diff --git a/tests/test_cli/test_repoinfo.py b/tests/test_cli/test_repoinfo.py new file mode 100644 index 0000000..cb54985 --- /dev/null +++ b/tests/test_cli/test_repoinfo.py @@ -0,0 +1,264 @@ +from __future__ import absolute_import + +import unittest + +import mock +import six +import tempfile + +from koji_cli.commands import anon_handle_repoinfo + +import koji +from . import utils + + +class TestRepoinfo(utils.CliTestCase): + + def __vm(self, result): + m = koji.VirtualCall('mcall_method', [], {}) + if isinstance(result, dict) and result.get('faultCode'): + m._result = result + else: + m._result = (result,) + return m + + def setUp(self): + # Show long diffs in error output... + self.maxDiff = None + self.options = mock.MagicMock() + self.options.debug = False + self.session = mock.MagicMock() + self.session.getAPIVersion.return_value = koji.API_VERSION + self.ensure_connection = mock.patch('koji_cli.commands.ensure_connection').start() + self.tempdir = tempfile.mkdtemp() + self.error_format = """Usage: %s repoinfo [options] [ ...] +(Specify the --help global option for a list of other help options) + +%s: error: {message} +""" % (self.progname, self.progname) + self.repo_id = '123' + self.multi_broots = [ + {'id': 1101, 'repo_id': 101, 'tag_name': 'tag_101', 'arch': 'x86_64'}, + {'id': 1111, 'repo_id': 111, 'tag_name': 'tag_111', 'arch': 'x86_64'}, + {'id': 1121, 'repo_id': 121, 'tag_name': 'tag_121', 'arch': 'x86_64'} + ] + + def tearDown(self): + mock.patch.stopall() + + @mock.patch('koji.formatTimeLong', return_value='Thu, 01 Jan 2000') + @mock.patch('sys.stderr', new_callable=six.StringIO) + @mock.patch('sys.stdout', new_callable=six.StringIO) + def test_repoinfo_valid_not_dist_repo_with_buildroot_opt(self, stdout, stderr, formattimelong): + repoinfo = {'external_repo_id': 1, 'id': self.repo_id, 'tag_id': 11, + 'tag_name': 'test-tag', 'state': 1, 'create_ts': 1632914520.353734, + 'create_event': 999, 'dist': False, 'task_id': 555} + self.options.topurl = 'https://www.domain.local' + mcall = self.session.multicall.return_value.__enter__.return_value + mcall.repoInfo.return_value = self.__vm(repoinfo) + self.session.listBuildroots.return_value = self.multi_broots + arguments = [self.repo_id, '--buildroots'] + rv = anon_handle_repoinfo(self.options, self.session, arguments) + url = '{}/repos/test-tag/123'.format(self.options.topurl) + repo_json = '{}/repos/test-tag/123/repo.json'.format(self.options.topurl) + expected = """ID: %s +Tag ID: %d +Tag name: %s +State: %s +Created: Thu, 01 Jan 2000 +Created event: %d +URL: %s +Repo json: %s +Dist repo?: no +Task ID: %d +Number of buildroots: 3 +Buildroots ID: + 1101 + 1111 + 1121 +""" % (self.repo_id, repoinfo['tag_id'], repoinfo['tag_name'], + koji.REPO_STATES[repoinfo['state']], repoinfo['create_event'], url, repo_json, + repoinfo['task_id']) + actual = stdout.getvalue() + self.assertMultiLineEqual(actual, expected) + actual = stderr.getvalue() + expected = '' + self.assertMultiLineEqual(actual, expected) + self.assertEqual(rv, None) + + self.ensure_connection.assert_called_once_with(self.session, self.options) + self.session.multicall.assert_called_once() + self.session.repoInfo.assert_not_called() + self.session.listBuildroots.assert_called_once_with(repoID=self.repo_id) + + @mock.patch('koji.formatTimeLong', return_value='Thu, 01 Jan 2000') + @mock.patch('sys.stderr', new_callable=six.StringIO) + @mock.patch('sys.stdout', new_callable=six.StringIO) + def test_repoinfo_valid_dist_repo(self, stdout, stderr, formattimelong): + repoinfo = {'external_repo_id': 1, 'id': self.repo_id, 'tag_id': 11, + 'tag_name': 'test-tag', 'state': 1, 'create_ts': 1632914520.353734, + 'create_event': 999, 'dist': True, 'task_id': 555} + mcall = self.session.multicall.return_value.__enter__.return_value + mcall.repoInfo.return_value = self.__vm(repoinfo) + self.session.listBuildroots.return_value = self.multi_broots + self.options.topurl = 'https://www.domain.local' + arguments = [self.repo_id] + rv = anon_handle_repoinfo(self.options, self.session, arguments) + url = '{}/repos/test-tag/123'.format(self.options.topurl) + repo_json = '{}/repos-dist/test-tag/123/repo.json'.format(self.options.topurl) + expected = """ID: %s +Tag ID: %d +Tag name: %s +State: %s +Created: Thu, 01 Jan 2000 +Created event: %d +URL: %s +Repo json: %s +Dist repo?: yes +Task ID: %d +Number of buildroots: 3 +""" % (self.repo_id, repoinfo['tag_id'], repoinfo['tag_name'], + koji.REPO_STATES[repoinfo['state']], repoinfo['create_event'], url, repo_json, + repoinfo['task_id']) + actual = stdout.getvalue() + self.assertMultiLineEqual(actual, expected) + actual = stderr.getvalue() + expected = '' + self.assertMultiLineEqual(actual, expected) + self.assertEqual(rv, None) + + self.ensure_connection.assert_called_once_with(self.session, self.options) + self.session.multicall.assert_called_once() + self.session.repoInfo.assert_not_called() + self.session.listBuildroots.assert_called_once_with(repoID=self.repo_id) + + @mock.patch('koji.formatTimeLong', return_value='Thu, 01 Jan 2000') + @mock.patch('sys.stderr', new_callable=six.StringIO) + @mock.patch('sys.stdout', new_callable=six.StringIO) + def test_repoinfo_valid_buildroot_not_available_on_hub(self, stdout, stderr, formattimelong): + repoinfo = {'external_repo_id': 1, 'id': self.repo_id, 'tag_id': 11, + 'tag_name': 'test-tag', 'state': 1, 'create_ts': 1632914520.353734, + 'create_event': 999, 'dist': False, 'task_id': 555} + self.options.topurl = 'https://www.domain.local' + mcall = self.session.multicall.return_value.__enter__.return_value + mcall.repoInfo.return_value = self.__vm(repoinfo) + self.session.listBuildroots.side_effect = koji.ParameterError + arguments = [self.repo_id, '--buildroots'] + rv = anon_handle_repoinfo(self.options, self.session, arguments) + url = '{}/repos/test-tag/123'.format(self.options.topurl) + repo_json = '{}/repos/test-tag/123/repo.json'.format(self.options.topurl) + expected = """ID: %s +Tag ID: %d +Tag name: %s +State: %s +Created: Thu, 01 Jan 2000 +Created event: %d +URL: %s +Repo json: %s +Dist repo?: no +Task ID: %d +""" % (self.repo_id, repoinfo['tag_id'], repoinfo['tag_name'], + koji.REPO_STATES[repoinfo['state']], repoinfo['create_event'], url, repo_json, + repoinfo['task_id']) + actual = stdout.getvalue() + self.assertMultiLineEqual(actual, expected) + actual = stderr.getvalue() + expecter_warn = "--buildroots option is available with hub 1.33 or newer\n" + self.assertMultiLineEqual(actual, expecter_warn) + self.assertEqual(rv, None) + + self.ensure_connection.assert_called_once_with(self.session, self.options) + self.session.multicall.assert_called_once() + self.session.repoInfo.assert_not_called() + self.session.listBuildroots.assert_called_once_with(repoID=self.repo_id) + + @mock.patch('koji.formatTimeLong', return_value='Thu, 01 Jan 2000') + @mock.patch('sys.stderr', new_callable=six.StringIO) + @mock.patch('sys.stdout', new_callable=six.StringIO) + def test_repoinfo_valid_without_buildroot_not_available_on_hub( + self, stdout, stderr, formattimelong): + repoinfo = {'external_repo_id': 1, 'id': self.repo_id, 'tag_id': 11, + 'tag_name': 'test-tag', 'state': 1, 'create_ts': 1632914520.353734, + 'create_event': 999, 'dist': False, 'task_id': 555} + self.options.topurl = 'https://www.domain.local' + mcall = self.session.multicall.return_value.__enter__.return_value + mcall.repoInfo.return_value = self.__vm(repoinfo) + self.session.listBuildroots.side_effect = koji.ParameterError + arguments = [self.repo_id] + rv = anon_handle_repoinfo(self.options, self.session, arguments) + url = '{}/repos/test-tag/123'.format(self.options.topurl) + repo_json = '{}/repos/test-tag/123/repo.json'.format(self.options.topurl) + expected = """ID: %s +Tag ID: %d +Tag name: %s +State: %s +Created: Thu, 01 Jan 2000 +Created event: %d +URL: %s +Repo json: %s +Dist repo?: no +Task ID: %d +""" % (self.repo_id, repoinfo['tag_id'], repoinfo['tag_name'], + koji.REPO_STATES[repoinfo['state']], repoinfo['create_event'], url, repo_json, + repoinfo['task_id']) + actual = stdout.getvalue() + self.assertMultiLineEqual(actual, expected) + actual = stderr.getvalue() + expecter_warn = "" + self.assertMultiLineEqual(actual, expecter_warn) + self.assertEqual(rv, None) + + self.ensure_connection.assert_called_once_with(self.session, self.options) + self.session.multicall.assert_called_once() + self.session.repoInfo.assert_not_called() + self.session.listBuildroots.assert_called_once_with(repoID=self.repo_id) + + @mock.patch('sys.stdout', new_callable=six.StringIO) + @mock.patch('sys.stderr', new_callable=six.StringIO) + def test_repoinfo__not_exist_repo(self, stderr, stdout): + mcall = self.session.multicall.return_value.__enter__.return_value + mcall.repoInfo.return_value = self.__vm(None) + arguments = [self.repo_id] + rv = anon_handle_repoinfo(self.options, self.session, arguments) + actual = stderr.getvalue() + expected = "No such repo: %s\n\n" % self.repo_id + self.assertMultiLineEqual(actual, expected) + actual = stdout.getvalue() + expected = '' + self.assertMultiLineEqual(actual, expected) + self.assertEqual(rv, None) + + self.ensure_connection.assert_called_once_with(self.session, self.options) + self.session.multicall.assert_called_once() + self.session.repoInfo.assert_not_called() + self.session.listBuildroots.assert_not_called() + + def test_repoinfo_without_args(self): + arguments = [] + # Run it and check immediate output + self.assert_system_exit( + anon_handle_repoinfo, + self.options, self.session, arguments, + stderr=self.format_error_message('Please specify a repo ID'), + stdout='', + activate_session=None, + exit_code=2) + + # Finally, assert that things were called as we expected. + self.ensure_connection.assert_not_called() + self.session.repoInfo.assert_not_called() + self.session.listBuildroots.assert_not_called() + + def test_repoinfo_help(self): + self.assert_help( + anon_handle_repoinfo, + """Usage: %s repoinfo [options] [ ...] +(Specify the --help global option for a list of other help options) + +Options: + -h, --help show this help message and exit + --buildroots Prints list of buildroot IDs +""" % self.progname) + + if __name__ == '__main__': + unittest.main() diff --git a/tests/test_cli/test_userinfo.py b/tests/test_cli/test_userinfo.py index 68ae0df..df4ed69 100644 --- a/tests/test_cli/test_userinfo.py +++ b/tests/test_cli/test_userinfo.py @@ -46,7 +46,7 @@ class TestUserinfo(utils.CliTestCase): self.assert_console_message(stderr, expected) @mock.patch('sys.stderr', new_callable=StringIO) - def test_userinfo_non_exist_tag(self, stderr): + def test_userinfo_non_exist_user(self, stderr): expected_warn = "No such user: %s\n\n" % self.user mcall = self.session.multicall.return_value.__enter__.return_value diff --git a/tests/test_hub/test_query_buildroots.py b/tests/test_hub/test_query_buildroots.py new file mode 100644 index 0000000..2257a55 --- /dev/null +++ b/tests/test_hub/test_query_buildroots.py @@ -0,0 +1,71 @@ +import unittest +import mock +import kojihub + +QP = kojihub.QueryProcessor + + +class TestQueryBuildroots(unittest.TestCase): + + def getQuery(self, *args, **kwargs): + query = QP(*args, **kwargs) + query.execute = self.query_execute + self.queries.append(query) + return query + + def setUp(self): + self.QueryProcessor = mock.patch('kojihub.kojihub.QueryProcessor', + side_effect=self.getQuery).start() + self.repo_references = mock.patch('kojihub.kojihub.repo_references').start() + self.queries = [] + self.query_execute = mock.MagicMock() + + def test_query_buildroots(self): + self.query_execute.side_effect = [[7], [7], [7], []] + self.repo_references.return_value = [{'id': 7, 'host_id': 1, 'create_event': 333, + 'state': 1}] + kojihub.query_buildroots(hostID=1, tagID=2, state=1, rpmID=3, archiveID=4, taskID=5, + buildrootID=7, repoID=10) + self.assertEqual(len(self.queries), 4) + query = self.queries[0] + self.assertEqual(query.tables, ['buildroot_listing']) + self.assertEqual(query.columns, ['buildroot_id']) + self.assertEqual(query.clauses, ['rpm_id = %(rpmID)i']) + self.assertEqual(query.joins, None) + query = self.queries[1] + self.assertEqual(query.tables, ['buildroot_archives']) + self.assertEqual(query.columns, ['buildroot_id']) + self.assertEqual(query.clauses, ['archive_id = %(archiveID)i']) + self.assertEqual(query.joins, None) + query = self.queries[2] + self.assertEqual(query.tables, ['standard_buildroot']) + self.assertEqual(query.columns, ['buildroot_id']) + self.assertEqual(query.clauses, ['task_id = %(taskID)i']) + self.assertEqual(query.joins, None) + query = self.queries[3] + self.assertEqual(query.tables, ['standard_buildroot']) + self.assertEqual(query.columns, ['buildroot_id']) + self.assertEqual(query.clauses, ['repo_id = %(repoID)i']) + self.assertEqual(query.joins, None) + + def test_query_buildroots_some_params_as_list(self): + kojihub.query_buildroots(state=[1], buildrootID=[7]) + self.assertEqual(len(self.queries), 1) + query = self.queries[0] + self.assertEqual(query.tables, ['buildroot']) + self.assertEqual(query.clauses, ['buildroot.id IN %(buildrootID)s', + 'standard_buildroot.state IN %(state)s']) + self.assertEqual(query.joins, + ['LEFT OUTER JOIN standard_buildroot ON ' + 'standard_buildroot.buildroot_id = buildroot.id', + 'LEFT OUTER JOIN content_generator ON ' + 'buildroot.cg_id = content_generator.id', + 'LEFT OUTER JOIN host ON host.id = standard_buildroot.host_id', + 'LEFT OUTER JOIN repo ON repo.id = standard_buildroot.repo_id', + 'LEFT OUTER JOIN tag ON tag.id = repo.tag_id', + 'LEFT OUTER JOIN events AS create_events ON ' + 'create_events.id = standard_buildroot.create_event', + 'LEFT OUTER JOIN events AS retire_events ON ' + 'standard_buildroot.retire_event = retire_events.id', + 'LEFT OUTER JOIN events AS repo_create ON ' + 'repo_create.id = repo.create_event']) diff --git a/www/kojiweb/buildroots.chtml b/www/kojiweb/buildroots.chtml new file mode 100644 index 0000000..6af0367 --- /dev/null +++ b/www/kojiweb/buildroots.chtml @@ -0,0 +1,97 @@ +#import koji +#from kojiweb import util + +#attr _PASSTHROUGH = ['repoID', 'order', 'state'] + +#include "includes/header.chtml" + +

Buildroots in repo $repoID

+ + + + + + + + + + + + + + + + #if $len($buildroots) > 0 + #for $buildroot in $buildroots + + + + + + #set $stateName = $util.brStateName($buildroot.state) + + + #end for + #else + + + + #end if + + + +
+ +
+ State: + + +
+
+ #if $len($buildrootPages) > 1 +
+ Page: + +
+ #end if + #if $buildrootStart > 0 + <<< + #end if + #if $totalBuildroots != 0 + Buildroots #echo $buildrootStart + 1 # through #echo $buildrootStart + $buildrootCount # of $totalBuildroots + #end if + #if $buildrootStart + $buildrootCount < $totalBuildroots + >>> + #end if +
BuildrootID $util.sortImage($self, 'id')Repo ID $util.sortImage($self, 'repo_id')Task ID $util.sortImage($self, 'task_id')Tag name $util.sortImage($self, 'tag_name')State $util.sortImage($self, 'state')
$buildroot.id$buildroot.repo_id$buildroot.task_id$util.escapeHTML($buildroot.tag_name)$util.brStateImage($buildroot.state)
No buildroots
+ #if $len($buildrootPages) > 1 +
+ Page: + +
+ #end if + #if $buildrootStart > 0 + <<< + #end if + #if $totalBuildroots != 0 + Buildroots #echo $buildrootStart + 1 # through #echo $buildrootStart + $buildrootCount # of $totalBuildroots + #end if + #if $buildrootStart + $buildrootCount < $totalBuildroots + >>> + #end if +
+ +#include "includes/footer.chtml" diff --git a/www/kojiweb/index.py b/www/kojiweb/index.py index 543873c..f565041 100644 --- a/www/kojiweb/index.py +++ b/www/kojiweb/index.py @@ -2659,6 +2659,8 @@ def repoinfo(environ, repoID): else: values['repo_json'] = os.path.join( pathinfo.repo(repo_info['id'], repo_info['tag_name']), 'repo.json') + num_buildroots = len(server.listBuildroots(repoID=repoID)) or 0 + values['numBuildroots'] = num_buildroots return _genHTML(environ, 'repoinfo.chtml') @@ -2692,3 +2694,21 @@ def activesessiondelete(environ, sessionID): server.logout(session_id=sessionID) _redirect(environ, 'activesession') + + +def buildroots(environ, repoID=None, order='id', start=None, state=None): + values = _initValues(environ, 'Buildroots', 'buildroots') + server = _getServer(environ) + values['repoID'] = repoID + values['order'] = order + if state == 'all': + state = None + elif state is not None: + state = int(state) + values['state'] = state + + kojiweb.util.paginateMethod(server, values, 'listBuildroots', + kw={'repoID': repoID, 'state': state}, start=start, + dataName='buildroots', prefix='buildroot', order=order) + + return _genHTML(environ, 'buildroots.chtml') diff --git a/www/kojiweb/repoinfo.chtml b/www/kojiweb/repoinfo.chtml index 6cb9d5a..2bf5c1d 100644 --- a/www/kojiweb/repoinfo.chtml +++ b/www/kojiweb/repoinfo.chtml @@ -20,6 +20,7 @@ Repo jsonrepo.json #end if Dist repo?#if $repo.dist then 'yes' else 'no'# + Number of buildroots: $numBuildroots #else Repo $repo_id not found. diff --git a/www/lib/kojiweb/util.py b/www/lib/kojiweb/util.py index 628a596..5720db8 100644 --- a/www/lib/kojiweb/util.py +++ b/www/lib/kojiweb/util.py @@ -433,6 +433,13 @@ def brStateName(stateID): return koji.BR_STATES[stateID].lower() +def brStateImage(stateID): + """Return an IMG tag that loads an icon appropriate for + the given state""" + name = brStateName(stateID) + return imageTag(name) + + def brLabel(brinfo): if brinfo['br_type'] == koji.BR_TYPES['STANDARD']: return '%(tag_name)s-%(id)i-%(repo_id)i' % brinfo