diff --git a/cli/koji_cli/commands.py b/cli/koji_cli/commands.py index 27760689..7ea648df 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 352a8d67..d8361d33 100644 --- a/kojihub/kojihub.py +++ b/kojihub/kojihub.py @@ -5586,7 +5586,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: @@ -5688,6 +5688,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 bdcd7397..c434b680 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 00000000..cb549859 --- /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 68ae0dfe..df4ed695 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 00000000..2257a55a --- /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 00000000..6af03679 --- /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 543873c9..f565041b 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 6cb9d5a7..2bf5c1d8 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 628a5962..5720db8e 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