From a040e62922239dad4bfce0375ad19ba76ab8fab9 Mon Sep 17 00:00:00 2001 From: Tester Date: Jun 13 2022 08:18:28 +0000 Subject: [PATCH 1/6] remove original commands.py --- diff --git a/cli/koji_cli/commands.py b/cli/koji_cli/commands.py deleted file mode 100644 index 42213c3..0000000 --- a/cli/koji_cli/commands.py +++ /dev/null @@ -1,8107 +0,0 @@ -from __future__ import absolute_import, division - -import ast -import fnmatch -import itertools -import json -import logging -import os -import pprint -import random -import stat -import sys -import textwrap -import time -import traceback -from datetime import datetime -from dateutil.tz import tzutc -from collections import OrderedDict, defaultdict -from optparse import SUPPRESS_HELP, OptionParser - -import six -import six.moves.xmlrpc_client -from six.moves import filter, map, range, zip - -import koji -from koji.util import base64encode, md5_constructor, to_list -from koji_cli.lib import ( - TimeOption, - DatetimeJSONEncoder, - _list_tasks, - _progress_callback, - _running_in_bg, - activate_session, - arg_filter, - download_archive, - download_file, - download_rpm, - ensure_connection, - error, - format_inheritance_flags, - get_usage_str, - greetings, - linked_upload, - list_task_output_all_volumes, - print_task_headers, - print_task_recurse, - unique_path, - warn, - watch_logs, - watch_tasks, - truncate_string -) - -try: - import libcomps -except ImportError: # pragma: no cover - libcomps = None - try: - import yum.comps as yumcomps - except ImportError: - yumcomps = None - - -def _printable_unicode(s): - if six.PY2: - return s.encode('utf-8') - else: # no cover: 2.x - return s - - -def handle_add_group(goptions, session, args): - "[admin] Add a group to a tag" - usage = "usage: %prog add-group " - parser = OptionParser(usage=get_usage_str(usage)) - (options, args) = parser.parse_args(args) - if len(args) != 2: - parser.error("Please specify a tag name and a group name") - tag = args[0] - group = args[1] - - activate_session(session, goptions) - if not (session.hasPerm('admin') or session.hasPerm('tag')): - parser.error("This action requires tag or admin privileges") - - dsttag = session.getTag(tag) - if not dsttag: - error("No such tag: %s" % tag) - - groups = dict([(p['name'], p['group_id']) for p in session.getTagGroups(tag, inherit=False)]) - group_id = groups.get(group, None) - if group_id is not None: - error("Group %s already exists for tag %s" % (group, tag)) - - session.groupListAdd(tag, group) - - -def handle_block_group(goptions, session, args): - "[admin] Block group in tag" - usage = "usage: %prog block-group " - parser = OptionParser(usage=get_usage_str(usage)) - (options, args) = parser.parse_args(args) - if len(args) != 2: - parser.error("Please specify a tag name and a group name") - tag = args[0] - group = args[1] - - activate_session(session, goptions) - if not (session.hasPerm('admin') or session.hasPerm('tag')): - parser.error("This action requires tag or admin privileges") - - dsttag = session.getTag(tag) - if not dsttag: - error("No such tag: %s" % tag) - - groups = dict([(p['name'], p['group_id']) for p in session.getTagGroups(tag, inherit=False)]) - group_id = groups.get(group, None) - if group_id is None: - error("Group %s doesn't exist within tag %s" % (group, tag)) - - session.groupListBlock(tag, group) - - -def handle_remove_group(goptions, session, args): - "[admin] Remove group from tag" - usage = "usage: %prog remove-group " - parser = OptionParser(usage=get_usage_str(usage)) - (options, args) = parser.parse_args(args) - if len(args) != 2: - parser.error("Please specify a tag name and a group name") - tag = args[0] - group = args[1] - - activate_session(session, goptions) - if not (session.hasPerm('admin') or session.hasPerm('tag')): - parser.error("This action requires tag or admin privileges") - - dsttag = session.getTag(tag) - if not dsttag: - error("No such tag: %s" % tag) - - groups = dict([(p['name'], p['group_id']) for p in session.getTagGroups(tag, inherit=False)]) - group_id = groups.get(group, None) - if group_id is None: - error("Group %s doesn't exist within tag %s" % (group, tag)) - - session.groupListRemove(tag, group) - - -def handle_assign_task(goptions, session, args): - "[admin] Assign a task to a host" - usage = 'usage: %prog assign-task ' - parser = OptionParser(usage=get_usage_str(usage)) - parser.add_option('-f', '--force', action='store_true', default=False, - help='force to assign a non-free task') - (options, args) = parser.parse_args(args) - - if len(args) != 2: - parser.error('please specify a task id and a hostname') - else: - task_id = int(args[0]) - hostname = args[1] - - taskinfo = session.getTaskInfo(task_id, request=False) - if taskinfo is None: - raise koji.GenericError("No such task: %s" % task_id) - - hostinfo = session.getHost(hostname) - if hostinfo is None: - raise koji.GenericError("No such host: %s" % hostname) - - force = False - if options.force: - force = True - - activate_session(session, goptions) - if not session.hasPerm('admin'): - parser.error("This action requires admin privileges") - - ret = session.assignTask(task_id, hostname, force) - if ret: - print('assigned task %d to host %s' % (task_id, hostname)) - else: - print('failed to assign task %d to host %s' % (task_id, hostname)) - - -def handle_add_host(goptions, session, args): - "[admin] Add a host" - usage = "usage: %prog add-host [options] [ ...]" - parser = OptionParser(usage=get_usage_str(usage)) - parser.add_option("--krb-principal", - help="set a non-default kerberos principal for the host") - parser.add_option("--force", default=False, action="store_true", - help="if existing used is a regular user, convert it to a host") - (options, args) = parser.parse_args(args) - if len(args) < 2: - parser.error("Please specify a hostname and at least one arch") - host = args[0] - activate_session(session, goptions) - id = session.getHost(host) - if id: - error("%s is already in the database" % host) - else: - kwargs = {'force': options.force} - if options.krb_principal is not None: - kwargs['krb_principal'] = options.krb_principal - id = session.addHost(host, args[1:], **kwargs) - print("%s added: id %d" % (host, id)) - - -def handle_edit_host(options, session, args): - "[admin] Edit a host" - usage = "usage: %prog edit-host [ ...] [options]" - parser = OptionParser(usage=get_usage_str(usage)) - parser.add_option("--arches", - help="Space or comma-separated list of supported architectures") - parser.add_option("--capacity", type="float", help="Capacity of this host") - parser.add_option("--description", metavar="DESC", help="Description of this host") - parser.add_option("--comment", help="A brief comment about this host") - (subopts, args) = parser.parse_args(args) - if not args: - parser.error("Please specify a hostname") - - activate_session(session, options) - - vals = {} - for key, val in subopts.__dict__.items(): - if val is not None: - vals[key] = val - if 'arches' in vals: - vals['arches'] = koji.parse_arches(vals['arches']) - - session.multicall = True - for host in args: - session.getHost(host) - error_hit = False - for host, [info] in zip(args, session.multiCall(strict=True)): - if not info: - warn("No such host: %s" % host) - error_hit = True - - if error_hit: - error("No changes made, please correct the command line") - - session.multicall = True - for host in args: - session.editHost(host, **vals) - for host, [result] in zip(args, session.multiCall(strict=True)): - if result: - print("Edited %s" % host) - else: - print("No changes made to %s" % host) - - -def handle_add_host_to_channel(goptions, session, args): - "[admin] Add a host to a channel" - usage = "usage: %prog add-host-to-channel [options] " - parser = OptionParser(usage=get_usage_str(usage)) - parser.add_option("--list", action="store_true", help=SUPPRESS_HELP) - parser.add_option("--new", action="store_true", help="Create channel if needed") - parser.add_option("--force", action="store_true", help="force added, if possible") - (options, args) = parser.parse_args(args) - if not options.list and len(args) != 2: - parser.error("Please specify a hostname and a channel") - activate_session(session, goptions) - if options.list: - for channel in session.listChannels(): - print(channel['name']) - return - channel = args[1] - if not options.new: - channelinfo = session.getChannel(channel) - if not channelinfo: - error("No such channel: %s" % channel) - - host = args[0] - hostinfo = session.getHost(host) - if not hostinfo: - error("No such host: %s" % host) - kwargs = {} - if options.new: - kwargs['create'] = True - if options.force: - kwargs['force'] = True - session.addHostToChannel(host, channel, **kwargs) - - -def handle_remove_host_from_channel(goptions, session, args): - "[admin] Remove a host from a channel" - usage = "usage: %prog remove-host-from-channel [options] " - parser = OptionParser(usage=get_usage_str(usage)) - (options, args) = parser.parse_args(args) - if len(args) != 2: - parser.error("Please specify a hostname and a channel") - host = args[0] - activate_session(session, goptions) - hostinfo = session.getHost(host) - if not hostinfo: - error("No such host: %s" % host) - hostchannels = [c['name'] for c in session.listChannels(hostinfo['id'])] - - channel = args[1] - if channel not in hostchannels: - error("Host %s is not a member of channel %s" % (host, channel)) - - session.removeHostFromChannel(host, channel) - - -def handle_add_channel(goptions, session, args): - "[admin] Add a channel" - usage = "usage: %prog add-channel [options] " - parser = OptionParser(usage=get_usage_str(usage)) - parser.add_option("--description", help="Description of channel") - (options, args) = parser.parse_args(args) - if len(args) != 1: - parser.error("Please specify one channel name") - activate_session(session, goptions) - channel_name = args[0] - try: - channel_id = session.addChannel(channel_name, description=options.description) - except koji.GenericError as ex: - msg = str(ex) - if 'channel %s already exists' % channel_name in msg: - error("channel %s already exists" % channel_name) - elif 'Invalid method:' in msg: - version = session.getKojiVersion() - error("addChannel is available on hub from Koji 1.26 version, your version is %s" % - version) - else: - error(msg) - print("%s added: id %d" % (args[0], channel_id)) - - -def handle_remove_channel(goptions, session, args): - "[admin] Remove a channel entirely" - usage = "usage: %prog remove-channel [options] " - parser = OptionParser(usage=get_usage_str(usage)) - parser.add_option("--force", action="store_true", help="force removal, if possible") - (options, args) = parser.parse_args(args) - print("remove-channel is deprecated and will be removed in 1.30") - if len(args) != 1: - parser.error("Incorrect number of arguments") - activate_session(session, goptions) - cinfo = session.getChannel(args[0]) - if not cinfo: - error("No such channel: %s" % args[0]) - session.removeChannel(args[0], force=options.force) - - -def handle_edit_channel(goptions, session, args): - "[admin] Edit a channel" - usage = "usage: %prog edit-channel [options] " - parser = OptionParser(usage=get_usage_str(usage)) - parser.add_option("--name", help="New channel name") - parser.add_option("--description", help="Description of channel") - parser.add_option("--comment", help="Comment of channel") - (options, args) = parser.parse_args(args) - if len(args) != 1: - parser.error("Incorrect number of arguments") - activate_session(session, goptions) - vals = {} - for key, val in options.__dict__.items(): - if val is not None: - vals[key] = val - cinfo = session.getChannel(args[0]) - if not cinfo: - error("No such channel: %s" % args[0]) - try: - result = session.editChannel(args[0], **vals) - except koji.GenericError as ex: - msg = str(ex) - if 'Invalid method:' in msg: - version = session.getKojiVersion() - error("editChannel is available on hub from Koji 1.26 version, your version is %s" % - version) - else: - print(msg) - if not result: - error("No changes made, please correct the command line") - - -def handle_enable_channel(goptions, session, args): - "[admin] Mark one or more channels as enabled" - usage = "usage: %prog enable-channel [options] [ ...]" - parser = OptionParser(usage=get_usage_str(usage)) - parser.add_option("--comment", help="Comment indicating why the channel(s) are being enabled") - (options, args) = parser.parse_args(args) - - if not args: - parser.error("At least one channel must be specified") - - activate_session(session, goptions) - with session.multicall() as m: - result = [m.getChannel(channel, strict=False) for channel in args] - error_hit = False - for channel, id in zip(args, result): - if not id.result: - print("No such channel: %s" % channel) - error_hit = True - if error_hit: - error("No changes made. Please correct the command line.") - - with session.multicall() as m: - [m.enableChannel(channel, comment=options.comment) for channel in args] - - -def handle_disable_channel(goptions, session, args): - "[admin] Mark one or more channels as disabled" - usage = "usage: %prog disable-channel [options] [ ...]" - parser = OptionParser(usage=get_usage_str(usage)) - parser.add_option("--comment", help="Comment indicating why the channel(s) are being disabled") - (options, args) = parser.parse_args(args) - - if not args: - parser.error("At least one channel must be specified") - - activate_session(session, goptions) - - with session.multicall() as m: - result = [m.getChannel(channel, strict=False) for channel in args] - error_hit = False - for channel, id in zip(args, result): - if not id.result: - print("No such channel: %s" % channel) - error_hit = True - if error_hit: - error("No changes made. Please correct the command line.") - with session.multicall() as m: - [m.disableChannel(channel, comment=options.comment) for channel in args] - - -def handle_add_pkg(goptions, session, args): - "[admin] Add a package to the listing for tag" - usage = "usage: %prog add-pkg [options] --owner [ ...]" - parser = OptionParser(usage=get_usage_str(usage)) - parser.add_option("--force", action='store_true', help="Override blocks if necessary") - parser.add_option("--owner", help="Specify owner") - parser.add_option("--extra-arches", help="Specify extra arches") - (options, args) = parser.parse_args(args) - if len(args) < 2: - parser.error("Please specify a tag and at least one package") - if not options.owner: - parser.error("Please specify an owner for the package(s)") - if not session.getUser(options.owner): - error("No such user: %s" % options.owner) - activate_session(session, goptions) - tag = args[0] - opts = {} - opts['force'] = options.force - opts['block'] = False - # check if list of packages exists for that tag already - dsttag = session.getTag(tag) - if dsttag is None: - error("No such tag: %s" % tag) - try: - pkglist = session.listPackages(tagID=dsttag['id'], with_owners=False) - except koji.ParameterError: - # performance option added in 1.25 - pkglist = session.listPackages(tagID=dsttag['id']) - pkglist = dict([(p['package_name'], p['package_id']) for p in pkglist]) - to_add = [] - for package in args[1:]: - package_id = pkglist.get(package, None) - if package_id is not None: - print("Package %s already exists in tag %s" % (package, tag)) - continue - to_add.append(package) - if options.extra_arches: - opts['extra_arches'] = koji.parse_arches(options.extra_arches) - - # add the packages - print("Adding %i packages to tag %s" % (len(to_add), dsttag['name'])) - session.multicall = True - for package in to_add: - session.packageListAdd(tag, package, options.owner, **opts) - session.multiCall(strict=True) - - -def handle_block_pkg(goptions, session, args): - "[admin] Block a package in the listing for tag" - usage = "usage: %prog block-pkg [options] [ ...]" - parser = OptionParser(usage=get_usage_str(usage)) - parser.add_option("--force", action='store_true', default=False, - help="Override blocks and owner if necessary") - (options, args) = parser.parse_args(args) - if len(args) < 2: - parser.error("Please specify a tag and at least one package") - activate_session(session, goptions) - tag = args[0] - # check if list of packages exists for that tag already - dsttag = session.getTag(tag) - if dsttag is None: - error("No such tag: %s" % tag) - try: - pkglist = session.listPackages(tagID=dsttag['id'], inherited=True, with_owners=False) - except koji.ParameterError: - # performance option added in 1.25 - pkglist = session.listPackages(tagID=dsttag['id'], inherited=True) - pkglist = dict([(p['package_name'], p['package_id']) for p in pkglist]) - ret = 0 - for package in args[1:]: - package_id = pkglist.get(package, None) - if package_id is None: - warn("Package %s doesn't exist in tag %s" % (package, tag)) - ret = 1 - if ret: - error(code=ret) - session.multicall = True - for package in args[1:]: - # force is not supported on older hub, so use it only explicitly - # https://pagure.io/koji/issue/1388 - if options.force: - session.packageListBlock(tag, package, force=options.force) - else: - session.packageListBlock(tag, package) - session.multiCall(strict=True) - - -def handle_remove_pkg(goptions, session, args): - "[admin] Remove a package from the listing for tag" - usage = "usage: %prog remove-pkg [options] [ ...]" - parser = OptionParser(usage=get_usage_str(usage)) - parser.add_option("--force", action='store_true', help="Override blocks if necessary") - (options, args) = parser.parse_args(args) - if len(args) < 2: - parser.error("Please specify a tag and at least one package") - activate_session(session, goptions) - tag = args[0] - opts = {} - opts['force'] = options.force - # check if list of packages exists for that tag already - dsttag = session.getTag(tag) - if dsttag is None: - error("No such tag: %s" % tag) - try: - pkglist = session.listPackages(tagID=dsttag['id'], with_owners=False) - except koji.ParameterError: - # performance option added in 1.25 - pkglist = session.listPackages(tagID=dsttag['id']) - pkglist = dict([(p['package_name'], p['package_id']) for p in pkglist]) - ret = 0 - for package in args[1:]: - package_id = pkglist.get(package, None) - if package_id is None: - warn("Package %s is not in tag %s" % (package, tag)) - ret = 1 - if ret: - error(code=ret) - session.multicall = True - for package in args[1:]: - session.packageListRemove(tag, package, **opts) - session.multiCall(strict=True) - - -def handle_build(options, session, args): - "[build] Build a package from source" - - usage = """\ - usage: %prog build [options] - - The first option is the build target, not to be confused with the destination - tag (where the build eventually lands) or build tag (where the buildroot - contents are pulled from). - - You can list all available build targets using the '%prog list-targets' command. - More detail can be found in the documentation. - https://docs.pagure.org/koji/HOWTO/#package-organization""" - - usage = textwrap.dedent(usage) - parser = OptionParser(usage=get_usage_str(usage)) - parser.add_option("--skip-tag", action="store_true", help="Do not attempt to tag package") - parser.add_option("--scratch", action="store_true", help="Perform a scratch build") - parser.add_option("--rebuild-srpm", action="store_true", dest="rebuild_srpm", - help="Force rebuilding SRPM for scratch build only") - parser.add_option("--no-rebuild-srpm", action="store_false", dest="rebuild_srpm", - help="Force not to rebuild srpm for scratch build only") - parser.add_option("--wait", action="store_true", - help="Wait on the build, even if running in the background") - parser.add_option("--nowait", action="store_false", dest="wait", help="Don't wait on build") - parser.add_option("--wait-repo", action="store_true", - help="Wait for the actual buildroot repo of given target") - parser.add_option("--wait-build", metavar="NVR", action="append", dest="wait_builds", - default=[], help="Wait for the given nvr to appear in buildroot repo") - parser.add_option("--quiet", action="store_true", - help="Do not print the task information", default=options.quiet) - parser.add_option("--arch-override", help="Override build arches") - parser.add_option("--fail-fast", action="store_true", - help="Override build_arch_can_fail settings and fail as fast as possible") - parser.add_option("--repo-id", type="int", help="Use a specific repo") - parser.add_option("--noprogress", action="store_true", - help="Do not display progress of the upload") - parser.add_option("--background", action="store_true", - help="Run the build at a lower priority") - parser.add_option("--custom-user-metadata", type="str", - help="Provide a JSON string of custom metadata to be deserialized and " - "stored under the build's extra.custom_user_metadata field") - (build_opts, args) = parser.parse_args(args) - if len(args) != 2: - parser.error("Exactly two arguments (a build target and a SCM URL or srpm file) are " - "required") - if build_opts.rebuild_srpm is not None and not build_opts.scratch: - parser.error("--no-/rebuild-srpm is only allowed for --scratch builds") - if build_opts.arch_override and not build_opts.scratch: - parser.error("--arch_override is only allowed for --scratch builds") - custom_user_metadata = {} - if build_opts.custom_user_metadata: - try: - custom_user_metadata = json.loads(build_opts.custom_user_metadata) - # Use ValueError instead of json.JSONDecodeError for Python 2 and 3 compatibility - except ValueError: - parser.error("--custom-user-metadata is not valid JSON") - - if not isinstance(custom_user_metadata, dict): - parser.error("--custom-user-metadata must be a JSON object") - - activate_session(session, options) - target = args[0] - if target.lower() == "none" and build_opts.repo_id: - target = None - build_opts.skip_tag = True - else: - build_target = session.getBuildTarget(target) - if not build_target: - parser.error("No such build target: %s" % target) - dest_tag = session.getTag(build_target['dest_tag']) - if not dest_tag: - parser.error("No such destination tag: %s" % build_target['dest_tag_name']) - if dest_tag['locked'] and not build_opts.scratch: - parser.error("Destination tag %s is locked" % dest_tag['name']) - source = args[1] - opts = {} - if build_opts.arch_override: - opts['arch_override'] = koji.parse_arches(build_opts.arch_override) - for key in ('skip_tag', 'scratch', 'repo_id', 'fail_fast', 'wait_repo', 'wait_builds', - 'rebuild_srpm'): - val = getattr(build_opts, key) - if val is not None: - opts[key] = val - opts["custom_user_metadata"] = custom_user_metadata - priority = None - if build_opts.background: - # relative to koji.PRIO_DEFAULT - priority = 5 - # try to check that source is an SRPM - if '://' not in source: - # treat source as an srpm and upload it - if not build_opts.quiet: - print("Uploading srpm: %s" % source) - serverdir = unique_path('cli-build') - if _running_in_bg() or build_opts.noprogress or build_opts.quiet: - callback = None - else: - callback = _progress_callback - session.uploadWrapper(source, serverdir, callback=callback) - print('') - source = "%s/%s" % (serverdir, os.path.basename(source)) - task_id = session.build(source, target, opts, priority=priority) - if not build_opts.quiet: - print("Created task: %d" % task_id) - print("Task info: %s/taskinfo?taskID=%s" % (options.weburl, task_id)) - if build_opts.wait or (build_opts.wait is None and not _running_in_bg()): - session.logout() - return watch_tasks(session, [task_id], quiet=build_opts.quiet, - poll_interval=options.poll_interval, topurl=options.topurl) - - -def handle_chain_build(options, session, args): - # XXX - replace handle_build with this, once chain-building has gotten testing - "[build] Build one or more packages from source" - usage = "usage: %prog chain-build [options] [ [:] [:] ...]" - parser = OptionParser(usage=get_usage_str(usage)) - parser.add_option("--wait", action="store_true", - help="Wait on build, even if running in the background") - parser.add_option("--nowait", action="store_false", dest="wait", help="Don't wait on build") - parser.add_option("--quiet", action="store_true", - help="Do not print the task information", default=options.quiet) - parser.add_option("--background", action="store_true", - help="Run the build at a lower priority") - (build_opts, args) = parser.parse_args(args) - if len(args) < 2: - parser.error("At least two arguments (a build target and a SCM URL) are required") - activate_session(session, options) - target = args[0] - build_target = session.getBuildTarget(target) - if not build_target: - parser.error("No such build target: %s" % target) - dest_tag = session.getTag(build_target['dest_tag'], strict=True) - if dest_tag['locked']: - parser.error("Destination tag %s is locked" % dest_tag['name']) - - # check that the destination tag is in the inheritance tree of the build tag - # otherwise there is no way that a chain-build can work - ancestors = session.getFullInheritance(build_target['build_tag']) - if dest_tag['id'] not in [build_target['build_tag']] + \ - [ancestor['parent_id'] for ancestor in ancestors]: - warn("Packages in destination tag %(dest_tag_name)s are not inherited by build tag " - "%(build_tag_name)s" % build_target) - error("Target %s is not usable for a chain-build" % build_target['name']) - sources = args[1:] - - src_list = [] - build_level = [] - # src_lists is a list of lists of sources to build. - # each list is block of builds ("build level") which must all be completed - # before the next block begins. Blocks are separated on the command line with ':' - for src in sources: - if src == ':': - if build_level: - src_list.append(build_level) - build_level = [] - elif '://' in src: - # quick check that src might be a url - build_level.append(src) - elif '/' not in src and not src.endswith('.rpm') and len(src.split('-')) >= 3: - # quick check that it looks like a N-V-R - build_level.append(src) - else: - error('"%s" is not a SCM URL or package N-V-R' % src) - if build_level: - src_list.append(build_level) - - if len(src_list) < 2: - parser.error('You must specify at least one dependency between builds with : (colon)\n' - 'If there are no dependencies, use the build command instead') - - priority = None - if build_opts.background: - # relative to koji.PRIO_DEFAULT - priority = 5 - - task_id = session.chainBuild(src_list, target, priority=priority) - if not build_opts.quiet: - print("Created task: %d" % task_id) - print("Task info: %s/taskinfo?taskID=%s" % (options.weburl, task_id)) - if build_opts.wait or (build_opts.wait is None and not _running_in_bg()): - session.logout() - return watch_tasks(session, [task_id], quiet=build_opts.quiet, - poll_interval=options.poll_interval, topurl=options.topurl) - - -def handle_maven_build(options, session, args): - "[build] Build a Maven package from source" - usage = "usage: %prog maven-build [options] " - usage += "\n %prog maven-build --ini=CONFIG... [options] " - parser = OptionParser(usage=get_usage_str(usage)) - parser.add_option("--patches", action="store", metavar="URL", - help="SCM URL of a directory containing patches to apply to the sources " - "before building") - parser.add_option("-G", "--goal", action="append", dest="goals", metavar="GOAL", default=[], - help="Additional goal to run before \"deploy\"") - parser.add_option("-P", "--profile", action="append", dest="profiles", metavar="PROFILE", - default=[], help="Enable a profile for the Maven build") - parser.add_option("-D", "--property", action="append", dest="properties", metavar="NAME=VALUE", - default=[], help="Pass a system property to the Maven build") - parser.add_option("-E", "--env", action="append", dest="envs", metavar="NAME=VALUE", - default=[], help="Set an environment variable") - parser.add_option("-p", "--package", action="append", dest="packages", metavar="PACKAGE", - default=[], help="Install an additional package into the buildroot") - parser.add_option("-J", "--jvm-option", action="append", dest="jvm_options", metavar="OPTION", - default=[], help="Pass a command-line option to the JVM") - parser.add_option("-M", "--maven-option", action="append", dest="maven_options", - metavar="OPTION", default=[], help="Pass a command-line option to Maven") - parser.add_option("--ini", action="append", dest="inis", metavar="CONFIG", default=[], - help="Pass build parameters via a .ini file") - parser.add_option("-s", "--section", help="Get build parameters from this section of the .ini") - parser.add_option("--debug", action="store_true", help="Run Maven build in debug mode") - parser.add_option("--specfile", action="store", metavar="URL", - help="SCM URL of a spec file fragment to use to generate wrapper RPMs") - parser.add_option("--skip-tag", action="store_true", help="Do not attempt to tag package") - parser.add_option("--scratch", action="store_true", help="Perform a scratch build") - parser.add_option("--wait", action="store_true", - help="Wait on build, even if running in the background") - parser.add_option("--nowait", action="store_false", dest="wait", help="Don't wait on build") - parser.add_option("--quiet", action="store_true", - help="Do not print the task information", default=options.quiet) - parser.add_option("--background", action="store_true", - help="Run the build at a lower priority") - (build_opts, args) = parser.parse_args(args) - if build_opts.inis: - if len(args) != 1: - parser.error("Exactly one argument (a build target) is required") - else: - if len(args) != 2: - parser.error("Exactly two arguments (a build target and a SCM URL) are required") - activate_session(session, options) - target = args[0] - build_target = session.getBuildTarget(target) - if not build_target: - parser.error("No such build target: %s" % target) - dest_tag = session.getTag(build_target['dest_tag']) - if not dest_tag: - parser.error("No such destination tag: %s" % build_target['dest_tag_name']) - if dest_tag['locked'] and not build_opts.scratch: - parser.error("Destination tag %s is locked" % dest_tag['name']) - if build_opts.inis: - try: - params = koji.util.parse_maven_param(build_opts.inis, scratch=build_opts.scratch, - section=build_opts.section) - except ValueError as e: - parser.error(e.args[0]) - opts = to_list(params.values())[0] - if opts.pop('type', 'maven') != 'maven': - parser.error("Section %s does not contain a maven-build config" % - to_list(params.keys())[0]) - source = opts.pop('scmurl') - else: - source = args[1] - opts = koji.util.maven_opts(build_opts, scratch=build_opts.scratch) - if '://' not in source: - parser.error("No such SCM URL: %s" % source) - if build_opts.debug: - opts.setdefault('maven_options', []).append('--debug') - if build_opts.skip_tag: - opts['skip_tag'] = True - priority = None - if build_opts.background: - # relative to koji.PRIO_DEFAULT - priority = 5 - task_id = session.mavenBuild(source, target, opts, priority=priority) - if not build_opts.quiet: - print("Created task: %d" % task_id) - print("Task info: %s/taskinfo?taskID=%s" % (options.weburl, task_id)) - if build_opts.wait or (build_opts.wait is None and not _running_in_bg()): - session.logout() - return watch_tasks(session, [task_id], quiet=build_opts.quiet, - poll_interval=options.poll_interval, topurl=options.topurl) - - -def handle_wrapper_rpm(options, session, args): - """[build] Build wrapper rpms for any archives associated with a build.""" - usage = "usage: %prog wrapper-rpm [options] " - parser = OptionParser(usage=get_usage_str(usage)) - parser.add_option("--create-build", action="store_true", - help="Create a new build to contain wrapper rpms") - parser.add_option("--ini", action="append", dest="inis", metavar="CONFIG", default=[], - help="Pass build parameters via a .ini file") - parser.add_option("-s", "--section", - help="Get build parameters from this section of the .ini") - parser.add_option("--skip-tag", action="store_true", - help="If creating a new build, don't tag it") - parser.add_option("--scratch", action="store_true", help="Perform a scratch build") - parser.add_option("--wait", action="store_true", - help="Wait on build, even if running in the background") - parser.add_option("--nowait", action="store_false", dest="wait", help="Don't wait on build") - parser.add_option("--background", action="store_true", - help="Run the build at a lower priority") - - (build_opts, args) = parser.parse_args(args) - if build_opts.inis: - if len(args) != 1: - parser.error("Exactly one argument (a build target) is required") - else: - if len(args) < 3: - parser.error("You must provide a build target, a build ID or NVR, " - "and a SCM URL to a specfile fragment") - activate_session(session, options) - - target = args[0] - if build_opts.inis: - try: - params = koji.util.parse_maven_param(build_opts.inis, scratch=build_opts.scratch, - section=build_opts.section) - except ValueError as e: - parser.error(e.args[0]) - opts = to_list(params.values())[0] - if opts.get('type') != 'wrapper': - parser.error("Section %s does not contain a wrapper-rpm config" % - to_list(params.keys())[0]) - url = opts['scmurl'] - package = opts['buildrequires'][0] - target_info = session.getBuildTarget(target, strict=True) - latest_builds = session.getLatestBuilds(target_info['dest_tag'], package=package) - if not latest_builds: - parser.error("No build of %s in %s" % (package, target_info['dest_tag_name'])) - build_id = latest_builds[0]['nvr'] - else: - build_id = args[1] - if build_id.isdigit(): - build_id = int(build_id) - url = args[2] - priority = None - if build_opts.background: - priority = 5 - opts = {} - if build_opts.create_build: - opts['create_build'] = True - if build_opts.skip_tag: - opts['skip_tag'] = True - if build_opts.scratch: - opts['scratch'] = True - task_id = session.wrapperRPM(build_id, url, target, priority, opts=opts) - print("Created task: %d" % task_id) - print("Task info: %s/taskinfo?taskID=%s" % (options.weburl, task_id)) - if build_opts.wait or (build_opts.wait is None and not _running_in_bg()): - session.logout() - return watch_tasks(session, [task_id], quiet=options.quiet, - poll_interval=options.poll_interval, topurl=options.topurl) - - -def handle_maven_chain(options, session, args): - "[build] Run a set of Maven builds in dependency order" - usage = "usage: %prog maven-chain [options] [ ...]" - parser = OptionParser(usage=get_usage_str(usage)) - parser.add_option("--skip-tag", action="store_true", help="Do not attempt to tag builds") - parser.add_option("--scratch", action="store_true", help="Perform scratch builds") - parser.add_option("--debug", action="store_true", help="Run Maven build in debug mode") - parser.add_option("--force", action="store_true", help="Force rebuilds of all packages") - parser.add_option("--wait", action="store_true", - help="Wait on build, even if running in the background") - parser.add_option("--nowait", action="store_false", dest="wait", help="Don't wait on build") - parser.add_option("--background", action="store_true", - help="Run the build at a lower priority") - (build_opts, args) = parser.parse_args(args) - if len(args) < 2: - parser.error("Two arguments (a build target and a config file) are required") - activate_session(session, options) - target = args[0] - build_target = session.getBuildTarget(target) - if not build_target: - parser.error("No such build target: %s" % target) - dest_tag = session.getTag(build_target['dest_tag']) - if not dest_tag: - parser.error("No such destination tag: %s" % build_target['dest_tag_name']) - if dest_tag['locked'] and not build_opts.scratch: - parser.error("Destination tag %s is locked" % dest_tag['name']) - opts = {} - for key in ('skip_tag', 'scratch', 'debug', 'force'): - val = getattr(build_opts, key) - if val: - opts[key] = val - try: - builds = koji.util.parse_maven_chain(args[1:], scratch=opts.get('scratch')) - except ValueError as e: - parser.error(e.args[0]) - priority = None - if build_opts.background: - priority = 5 - task_id = session.chainMaven(builds, target, opts, priority=priority) - print("Created task: %d" % task_id) - print("Task info: %s/taskinfo?taskID=%s" % (options.weburl, task_id)) - if build_opts.wait or (build_opts.wait is None and not _running_in_bg()): - session.logout() - return watch_tasks(session, [task_id], quiet=options.quiet, - poll_interval=options.poll_interval, topurl=options.topurl) - - -def handle_resubmit(goptions, session, args): - """[build] Retry a canceled or failed task, using the same parameter as the original task.""" - usage = "usage: %prog resubmit [options] " - parser = OptionParser(usage=get_usage_str(usage)) - parser.add_option("--wait", action="store_true", - help="Wait on task, even if running in the background") - parser.add_option("--nowait", action="store_false", dest="wait", help="Don't wait on task") - parser.add_option("--nowatch", action="store_true", dest="nowait", - help="An alias for --nowait") - parser.add_option("--quiet", action="store_true", default=goptions.quiet, - help="Do not print the task information") - (options, args) = parser.parse_args(args) - if len(args) != 1: - parser.error("Please specify a single task ID") - activate_session(session, goptions) - taskID = int(args[0]) - if not options.quiet: - print("Resubmitting the following task:") - _printTaskInfo(session, taskID, goptions.topdir, 0, False, True) - newID = session.resubmitTask(taskID) - if not options.quiet: - print("Resubmitted task %s as new task %s" % (taskID, newID)) - if options.wait or (options.wait is None and not _running_in_bg()): - session.logout() - return watch_tasks(session, [newID], quiet=options.quiet, - poll_interval=goptions.poll_interval, topurl=goptions.topurl) - - -def handle_call(goptions, session, args): - "Execute an arbitrary XML-RPC call" - usage = """\ - usage: %prog call [options] [ ...] - - Note, that you can use global option --noauth for anonymous calls here""" - usage = textwrap.dedent(usage) - parser = OptionParser(usage=get_usage_str(usage)) - parser.add_option("--python", action="store_true", help="Use python syntax for values") - parser.add_option("--kwargs", - help="Specify keyword arguments as a dictionary (implies --python)") - parser.add_option("--json-output", action="store_true", help="Use JSON syntax for output") - (options, args) = parser.parse_args(args) - if len(args) < 1: - parser.error("Please specify the name of the XML-RPC method") - if options.kwargs: - options.python = True - if options.python and ast is None: - parser.error("The ast module is required to read python syntax") - if options.json_output and json is None: - parser.error("The json module is required to output JSON syntax") - activate_session(session, goptions) - name = args[0] - non_kw = [] - kw = {} - if options.python: - non_kw = [ast.literal_eval(a) for a in args[1:]] - if options.kwargs: - kw = ast.literal_eval(options.kwargs) - else: - for arg in args[1:]: - if arg.find('=') != -1: - key, value = arg.split('=', 1) - kw[key] = arg_filter(value) - else: - non_kw.append(arg_filter(arg)) - response = getattr(session, name).__call__(*non_kw, **kw) - if options.json_output: - print(json.dumps(response, indent=2, separators=(',', ': '), cls=DatetimeJSONEncoder)) - else: - pprint.pprint(response) - - -def anon_handle_mock_config(goptions, session, args): - "[info] Create a mock config" - usage = "usage: %prog mock-config [options]" - parser = OptionParser(usage=get_usage_str(usage)) - parser.add_option("-a", "--arch", help="Specify the arch") - parser.add_option("-n", "--name", help="Specify the name for the buildroot") - parser.add_option("--tag", help="Create a mock config for a tag") - parser.add_option("--target", help="Create a mock config for a build target") - parser.add_option("--task", help="Duplicate the mock config of a previous task") - parser.add_option("--latest", action="store_true", help="use the latest redirect url") - parser.add_option("--buildroot", - help="Duplicate the mock config for the specified buildroot id") - parser.add_option("--mockdir", default="/var/lib/mock", metavar="DIR", help="Specify mockdir") - parser.add_option("--topdir", metavar="DIR", - help="Specify topdir, topdir tops the topurl") - parser.add_option("--topurl", metavar="URL", - help="URL under which Koji files are accessible, " - "when topdir is specified, topdir tops the topurl") - parser.add_option("--distribution", default="Koji Testing", - help="Change the distribution macro") - parser.add_option("--yum-proxy", help="Specify a yum proxy") - parser.add_option("-o", metavar="FILE", dest="ofile", help="Output to a file") - (options, args) = parser.parse_args(args) - ensure_connection(session, goptions) - if args: - # for historical reasons, we also accept buildroot name as first arg - if not options.name: - options.name = args[0] - else: - parser.error("Name already specified via option") - arch = None - opts = {} - for k in ('topdir', 'topurl', 'distribution', 'mockdir', 'yum_proxy'): - if hasattr(options, k): - if getattr(options, k) is not None: - opts[k] = getattr(options, k) - if opts.get('topdir') and opts.get('topurl'): - del opts['topurl'] - if not opts.get('topdir') and not opts.get('topurl'): - opts['topurl'] = goptions.topurl - if options.buildroot: - try: - br_id = int(options.buildroot) - except ValueError: - parser.error("Buildroot id must be an integer") - brootinfo = session.getBuildroot(br_id) - if brootinfo is None: - error("No such buildroot: %r" % br_id) - if options.latest: - opts['repoid'] = 'latest' - else: - opts['repoid'] = brootinfo['repo_id'] - opts['tag_name'] = brootinfo['tag_name'] - arch = brootinfo['arch'] - elif options.task: - try: - task_id = int(options.task) - except ValueError: - parser.error("Task id must be an integer") - broots = session.listBuildroots(taskID=task_id) - if not broots: - error("No buildroots for task %s (or no such task)" % options.task) - if len(broots) > 1: - print("Multiple buildroots found: %s" % [br['id'] for br in broots]) - brootinfo = broots[-1] - if options.latest: - opts['repoid'] = 'latest' - else: - opts['repoid'] = brootinfo['repo_id'] - opts['tag_name'] = brootinfo['tag_name'] - arch = brootinfo['arch'] - if not options.name: - options.name = "%s-task_%i" % (opts['tag_name'], task_id) - elif options.tag: - if not options.arch: - error("Please specify an arch") - tag = session.getTag(options.tag) - if not tag: - parser.error("No such tag: %s" % options.tag) - arch = options.arch - config = session.getBuildConfig(tag['id']) - if not config: - error("Could not get config info for tag: %(name)s" % tag) - opts['tag_name'] = tag['name'] - if options.latest: - opts['repoid'] = 'latest' - else: - repo = session.getRepo(config['id']) - if not repo: - error("Could not get a repo for tag: %(name)s" % tag) - opts['repoid'] = repo['id'] - elif options.target: - if not options.arch: - error("Please specify an arch") - arch = options.arch - target = session.getBuildTarget(options.target) - if not target: - parser.error("No such build target: %s" % options.target) - opts['tag_name'] = target['build_tag_name'] - if options.latest: - opts['repoid'] = 'latest' - else: - repo = session.getRepo(target['build_tag']) - if not repo: - error("Could not get a repo for tag: %s" % opts['tag_name']) - opts['repoid'] = repo['id'] - else: - parser.error("Please specify one of: --tag, --target, --task, --buildroot") - if options.name: - name = options.name - else: - name = "%(tag_name)s-repo_%(repoid)s" % opts - - event = None - if opts['repoid'] != 'latest': - event = session.repoInfo(opts['repoid'])['create_event'] - buildcfg = session.getBuildConfig(opts['tag_name'], event=event) - if arch: - if not buildcfg['arches']: - warn("Tag %s has an empty arch list" % opts['tag_name']) - elif arch not in buildcfg['arches']: - warn('%s is not in the list of tag arches' % arch) - if 'mock.forcearch' in buildcfg['extra']: - if bool(buildcfg['extra']['mock.forcearch']): - opts['forcearch'] = arch - if 'mock.package_manager' in buildcfg['extra']: - opts['package_manager'] = buildcfg['extra']['mock.package_manager'] - if 'mock.yum.module_hotfixes' in buildcfg['extra']: - opts['module_hotfixes'] = buildcfg['extra']['mock.yum.module_hotfixes'] - if 'mock.yum.best' in buildcfg['extra']: - opts['yum_best'] = int(buildcfg['extra']['mock.yum.best']) - if 'mock.bootstrap_image' in buildcfg['extra']: - opts['use_bootstrap_image'] = True - opts['bootstrap_image'] = buildcfg['extra']['mock.bootstrap_image'] - if 'mock.use_bootstrap' in buildcfg['extra']: - opts['use_bootstrap'] = buildcfg['extra']['mock.use_bootstrap'] - if 'mock.module_setup_commands' in buildcfg['extra']: - opts['module_setup_commands'] = buildcfg['extra']['mock.module_setup_commands'] - if 'mock.releasever' in buildcfg['extra']: - opts['releasever'] = buildcfg['extra']['mock.releasever'] - opts['tag_macros'] = {} - for key in buildcfg['extra']: - if key.startswith('rpm.macro.'): - macro = '%' + key[10:] - opts['tag_macros'][macro] = buildcfg['extra'][key] - output = koji.genMockConfig(name, arch, **opts) - if options.ofile: - with open(options.ofile, 'wt') as fo: - fo.write(output) - else: - print(output) - - -def handle_disable_host(goptions, session, args): - "[admin] Mark one or more hosts as disabled" - usage = "usage: %prog disable-host [options] [ ...]" - parser = OptionParser(usage=get_usage_str(usage)) - parser.add_option("--comment", help="Comment indicating why the host(s) are being disabled") - (options, args) = parser.parse_args(args) - - if not args: - parser.error("At least one host must be specified") - - activate_session(session, goptions) - session.multicall = True - for host in args: - session.getHost(host) - error_hit = False - for host, [id] in zip(args, session.multiCall(strict=True)): - if not id: - print("No such host: %s" % host) - error_hit = True - if error_hit: - error("No changes made. Please correct the command line.") - session.multicall = True - for host in args: - session.disableHost(host) - if options.comment is not None: - session.editHost(host, comment=options.comment) - session.multiCall(strict=True) - - -def handle_enable_host(goptions, session, args): - "[admin] Mark one or more hosts as enabled" - usage = "usage: %prog enable-host [options] [ ...]" - parser = OptionParser(usage=get_usage_str(usage)) - parser.add_option("--comment", help="Comment indicating why the host(s) are being enabled") - (options, args) = parser.parse_args(args) - - if not args: - parser.error("At least one host must be specified") - - activate_session(session, goptions) - session.multicall = True - for host in args: - session.getHost(host) - error_hit = False - for host, [id] in zip(args, session.multiCall(strict=True)): - if not id: - print("No such host: %s" % host) - error_hit = True - if error_hit: - error("No changes made. Please correct the command line.") - session.multicall = True - for host in args: - session.enableHost(host) - if options.comment is not None: - session.editHost(host, comment=options.comment) - session.multiCall(strict=True) - - -def handle_restart_hosts(options, session, args): - "[admin] Restart enabled hosts" - usage = "usage: %prog restart-hosts [options]" - parser = OptionParser(usage=get_usage_str(usage)) - parser.add_option("--wait", action="store_true", - help="Wait on the task, even if running in the background") - parser.add_option("--nowait", action="store_false", dest="wait", help="Don't wait on task") - parser.add_option("--quiet", action="store_true", - help="Do not print the task information", default=options.quiet) - parser.add_option("--force", action="store_true", help="Ignore checks and force operation") - parser.add_option("--channel", help="Only hosts in this channel") - parser.add_option("--arch", "-a", action="append", default=[], - help="Limit to hosts of this architecture (can be given multiple times)") - parser.add_option("--timeout", metavar='N', type='int', help="Time out after N seconds") - (my_opts, args) = parser.parse_args(args) - - if len(args) > 0: - parser.error("restart-hosts does not accept arguments") - - activate_session(session, options) - - # check for existing restart tasks - if not my_opts.force: - query = { - 'method': 'restartHosts', - 'state': - [koji.TASK_STATES[s] for s in ('FREE', 'OPEN', 'ASSIGNED')], - } - others = session.listTasks(query) - if others: - warn('Found other restartHosts tasks running.') - warn('Task ids: %r' % [t['id'] for t in others]) - error('Use --force to run anyway') - - callopts = {} - if my_opts.channel: - callopts['channel'] = my_opts.channel - if my_opts.arch: - callopts['arches'] = my_opts.arch - if my_opts.timeout: - callopts['timeout'] = my_opts.timeout - if callopts: - task_id = session.restartHosts(options=callopts) - else: - # allow default case to work with older hub - task_id = session.restartHosts() - if my_opts.wait or (my_opts.wait is None and not _running_in_bg()): - session.logout() - return watch_tasks(session, [task_id], quiet=my_opts.quiet, - poll_interval=options.poll_interval, topurl=options.topurl) - - -def handle_import(goptions, session, args): - "[admin] Import externally built RPMs into the database" - usage = "usage: %prog import [options] [ ...]" - parser = OptionParser(usage=get_usage_str(usage)) - parser.add_option("--link", action="store_true", - help="Attempt to hardlink instead of uploading") - parser.add_option("--test", action="store_true", help="Don't actually import") - parser.add_option("--create-build", action="store_true", help="Auto-create builds as needed") - parser.add_option("--src-epoch", help="When auto-creating builds, use this epoch") - (options, args) = parser.parse_args(args) - if len(args) < 1: - parser.error("At least one package must be specified") - if options.src_epoch in ('None', 'none', '(none)'): - options.src_epoch = None - elif options.src_epoch: - try: - options.src_epoch = int(options.src_epoch) - except (ValueError, TypeError): - parser.error("Invalid value for epoch: %s" % options.src_epoch) - activate_session(session, goptions) - to_import = {} - for path in args: - data = koji.get_header_fields(path, ('name', 'version', 'release', 'epoch', - 'arch', 'sigmd5', 'sourcepackage', 'sourcerpm')) - if data['sourcepackage']: - data['arch'] = 'src' - nvr = "%(name)s-%(version)s-%(release)s" % data - else: - nvr = "%(name)s-%(version)s-%(release)s" % koji.parse_NVRA(data['sourcerpm']) - to_import.setdefault(nvr, []).append((path, data)) - builds_missing = False - nvrs = sorted(to_list(to_import.keys())) - for nvr in nvrs: - to_import[nvr].sort() - for path, data in to_import[nvr]: - if data['sourcepackage']: - break - else: - # no srpm included, check for build - binfo = session.getBuild(nvr) - if not binfo: - print("Missing build or srpm: %s" % nvr) - builds_missing = True - if builds_missing and not options.create_build: - print("Aborting import") - return - - # local function to help us out below - def do_import(path, data): - rinfo = dict([(k, data[k]) for k in ('name', 'version', 'release', 'arch')]) - prev = session.getRPM(rinfo) - if prev and not prev.get('external_repo_id', 0): - if prev['payloadhash'] == koji.hex_string(data['sigmd5']): - print("RPM already imported: %s" % path) - else: - warn("md5sum mismatch for %s" % path) - warn(" A different rpm with the same name has already been imported") - warn(" Existing sigmd5 is %r, your import has %r" % ( - prev['payloadhash'], koji.hex_string(data['sigmd5']))) - print("Skipping import") - return - if options.test: - print("Test mode -- skipping import for %s" % path) - return - serverdir = unique_path('cli-import') - if options.link: - linked_upload(path, serverdir) - else: - sys.stdout.write("uploading %s... " % path) - sys.stdout.flush() - session.uploadWrapper(path, serverdir) - print("done") - sys.stdout.flush() - sys.stdout.write("importing %s... " % path) - sys.stdout.flush() - try: - session.importRPM(serverdir, os.path.basename(path)) - except koji.GenericError as e: - print("\nError importing: %s" % str(e).splitlines()[-1]) - sys.stdout.flush() - else: - print("done") - sys.stdout.flush() - - for nvr in nvrs: - # check for existing build - need_build = True - binfo = session.getBuild(nvr) - if binfo: - b_state = koji.BUILD_STATES[binfo['state']] - if b_state == 'COMPLETE': - need_build = False - elif b_state in ['FAILED', 'CANCELED']: - if not options.create_build: - print("Build %s state is %s. Skipping import" % (nvr, b_state)) - continue - else: - print("Build %s exists with state=%s. Skipping import" % (nvr, b_state)) - continue - - # import srpms first, if any - for path, data in to_import[nvr]: - if data['sourcepackage']: - if binfo and b_state != 'COMPLETE': - # need to fix the state - print("Creating empty build: %s" % nvr) - b_data = koji.util.dslice(binfo, ['name', 'version', 'release']) - b_data['epoch'] = data['epoch'] - session.createEmptyBuild(**b_data) - binfo = session.getBuild(nvr) - do_import(path, data) - need_build = False - - if need_build: - # if we're doing this here, we weren't given the matching srpm - if not options.create_build: # pragma: no cover - if binfo: - # should have caught this earlier, but just in case... - b_state = koji.BUILD_STATES[binfo['state']] - print("Build %s state is %s. Skipping import" % (nvr, b_state)) - continue - else: - print("No such build: %s (include matching srpm or use " - "--create-build option to add it)" % nvr) - continue - else: - # let's make a new build - b_data = koji.parse_NVR(nvr) - if options.src_epoch: - b_data['epoch'] = options.src_epoch - else: - # pull epoch from first rpm - data = to_import[nvr][0][1] - b_data['epoch'] = data['epoch'] - if options.test: - print("Test mode -- would have created empty build: %s" % nvr) - else: - print("Creating empty build: %s" % nvr) - session.createEmptyBuild(**b_data) - binfo = session.getBuild(nvr) - - for path, data in to_import[nvr]: - if data['sourcepackage']: - continue - do_import(path, data) - - -def handle_import_cg(goptions, session, args): - "[admin] Import external builds with rich metadata" - usage = "usage: %prog import-cg [options] " - parser = OptionParser(usage=get_usage_str(usage)) - parser.add_option("--noprogress", action="store_true", - help="Do not display progress of the upload") - parser.add_option("--link", action="store_true", - help="Attempt to hardlink instead of uploading") - parser.add_option("--test", action="store_true", help="Don't actually import") - parser.add_option("--token", action="store", default=None, help="Build reservation token") - (options, args) = parser.parse_args(args) - if len(args) < 2: - parser.error("Please specify metadata files directory") - activate_session(session, goptions) - metadata = koji.load_json(args[0]) - if 'output' not in metadata: - error("Metadata contains no output") - localdir = args[1] - - to_upload = [] - for info in metadata['output']: - if info.get('metadata_only', False): - continue - localpath = os.path.join(localdir, info.get('relpath', ''), info['filename']) - if not os.path.exists(localpath): - parser.error("No such file: %s" % localpath) - to_upload.append([localpath, info]) - - if options.test: - return - - # get upload path - # XXX - need a better way - serverdir = unique_path('cli-import') - - for localpath, info in to_upload: - relpath = os.path.join(serverdir, info.get('relpath', '')) - if _running_in_bg() or options.noprogress: - callback = None - else: - callback = _progress_callback - if options.link: - linked_upload(localpath, relpath) - else: - print("Uploading %s" % localpath) - session.uploadWrapper(localpath, relpath, callback=callback) - if callback: - print('') - - session.CGImport(metadata, serverdir, options.token) - - -def handle_import_comps(goptions, session, args): - "Import group/package information from a comps file" - usage = "usage: %prog import-comps [options] " - parser = OptionParser(usage=get_usage_str(usage)) - parser.add_option("--force", action="store_true", help="force import") - (local_options, args) = parser.parse_args(args) - if len(args) != 2: - parser.error("Incorrect number of arguments") - activate_session(session, goptions) - # check if the tag exists - dsttag = session.getTag(args[1]) - if dsttag is None: - error("No such tag: %s" % args[1]) - if libcomps is not None: - _import_comps(session, args[0], args[1], local_options) - elif yumcomps is not None: - _import_comps_alt(session, args[0], args[1], local_options) - else: - error("comps module not available") - - -def _import_comps(session, filename, tag, options): - """Import comps data using libcomps module""" - comps = libcomps.Comps() - comps.fromxml_f(filename) - force = options.force - ptypes = { - libcomps.PACKAGE_TYPE_DEFAULT: 'default', - libcomps.PACKAGE_TYPE_OPTIONAL: 'optional', - libcomps.PACKAGE_TYPE_CONDITIONAL: 'conditional', - libcomps.PACKAGE_TYPE_MANDATORY: 'mandatory', - libcomps.PACKAGE_TYPE_UNKNOWN: 'unknown', - } - for group in comps.groups: - print("Group: %s (%s)" % (group.id, group.name)) - session.groupListAdd( - tag, group.id, force=force, display_name=group.name, - is_default=bool(group.default), - uservisible=bool(group.uservisible), - description=group.desc, - langonly=group.lang_only, - biarchonly=bool(group.biarchonly)) - for pkg in group.packages: - pkgopts = {'type': ptypes[pkg.type], - 'basearchonly': bool(pkg.basearchonly), - } - if pkg.type == libcomps.PACKAGE_TYPE_CONDITIONAL: - pkgopts['requires'] = pkg.requires - for k in pkgopts.keys(): - if six.PY2 and isinstance(pkgopts[k], unicode): # noqa: F821 - pkgopts[k] = str(pkgopts[k]) - s_opts = ', '.join(["'%s': %r" % (k, pkgopts[k]) for k in sorted(pkgopts.keys())]) - print(" Package: %s: {%s}" % (pkg.name, s_opts)) - session.groupPackageListAdd(tag, group.id, pkg.name, force=force, **pkgopts) - # libcomps does not support group dependencies - # libcomps does not support metapkgs - - -def _import_comps_alt(session, filename, tag, options): # no cover 3.x - """Import comps data using yum.comps module""" - print('WARN: yum.comps does not support the biarchonly of group and basearchonly of package') - comps = yumcomps.Comps() - comps.add(filename) - force = options.force - for group in comps.groups: - print("Group: %(groupid)s (%(name)s)" % vars(group)) - session.groupListAdd(tag, group.groupid, force=force, display_name=group.name, - is_default=bool(group.default), - uservisible=bool(group.user_visible), - description=group.description, - langonly=group.langonly) - # yum.comps does not support the biarchonly field - for ptype, pdata in [('mandatory', group.mandatory_packages), - ('default', group.default_packages), - ('optional', group.optional_packages), - ('conditional', group.conditional_packages)]: - for pkg in pdata: - # yum.comps does not support basearchonly - pkgopts = {'type': ptype} - if ptype == 'conditional': - pkgopts['requires'] = pdata[pkg] - for k in pkgopts.keys(): - if six.PY2 and isinstance(pkgopts[k], unicode): # noqa: F821 - pkgopts[k] = str(pkgopts[k]) - s_opts = ', '.join(["'%s': %r" % (k, pkgopts[k]) for k in sorted(pkgopts.keys())]) - print(" Package: %s: {%s}" % (pkg, s_opts)) - session.groupPackageListAdd(tag, group.groupid, pkg, force=force, **pkgopts) - # yum.comps does not support group dependencies - # yum.comps does not support metapkgs - - -def handle_import_sig(goptions, session, args): - "[admin] Import signatures into the database and write signed RPMs" - usage = "usage: %prog import-sig [options] [ ...]" - parser = OptionParser(usage=get_usage_str(usage)) - parser.add_option("--with-unsigned", action="store_true", - help="Also import unsigned sig headers") - parser.add_option("--write", action="store_true", help=SUPPRESS_HELP) - parser.add_option("--test", action="store_true", help="Test mode -- don't actually import") - (options, args) = parser.parse_args(args) - if len(args) < 1: - parser.error("At least one package must be specified") - for path in args: - if not os.path.exists(path): - parser.error("No such file: %s" % path) - activate_session(session, goptions) - for path in args: - data = koji.get_header_fields(path, ('name', 'version', 'release', 'arch', 'siggpg', - 'sigpgp', 'dsaheader', 'rsaheader', - 'sourcepackage')) - if data['sourcepackage']: - data['arch'] = 'src' - sigkey = data['siggpg'] - if not sigkey: - sigkey = data['sigpgp'] - if not sigkey: - sigkey = data['dsaheader'] - if not sigkey: - sigkey = data['rsaheader'] - if not sigkey: - sigkey = "" - if not options.with_unsigned: - print("Skipping unsigned package: %s" % path) - continue - else: - sigkey = koji.get_sigpacket_key_id(sigkey) - del data['siggpg'] - del data['sigpgp'] - del data['dsaheader'] - del data['rsaheader'] - rinfo = session.getRPM(data) - if not rinfo: - print("No such rpm in system: %(name)s-%(version)s-%(release)s.%(arch)s" % data) - continue - if rinfo.get('external_repo_id'): - print("Skipping external rpm: %(name)s-%(version)s-%(release)s.%(arch)s@" - "%(external_repo_name)s" % rinfo) - continue - sighdr = koji.rip_rpm_sighdr(path) - previous = session.queryRPMSigs(rpm_id=rinfo['id'], sigkey=sigkey) - assert len(previous) <= 1 - if previous: - sighash = md5_constructor(sighdr).hexdigest() - if previous[0]['sighash'] == sighash: - print("Signature already imported: %s" % path) - continue - else: - warn("signature mismatch: %s" % path) - warn(" The system already has a signature for this rpm with key %s" % sigkey) - warn(" The two signature headers are not the same") - continue - print("Importing signature [key %s] from %s..." % (sigkey, path)) - if not options.test: - session.addRPMSig(rinfo['id'], base64encode(sighdr)) - print("Writing signed copy") - if not options.test: - session.writeSignedRPM(rinfo['id'], sigkey) - - -def handle_remove_sig(goptions, session, args): - "[admin] Remove signed RPMs from db and disk" - usage = "usage: %prog remove-sig [options] " - description = "Only use this method in extreme situations, because it " - description += "goes against Koji's design of immutable, auditable data." - parser = OptionParser(usage=get_usage_str(usage), description=description) - parser.add_option("--sigkey", action="store", default=None, help="Specify signature key") - parser.add_option("--all", action="store_true", default=False, - help="Remove all signed copies for specified RPM") - (options, args) = parser.parse_args(args) - if len(args) < 1: - parser.error("Please specify an RPM") - - if not options.all and not options.sigkey: - error("Either --sigkey or --all options must be given") - - if options.all and options.sigkey: - error("Conflicting options specified") - - activate_session(session, goptions) - rpminfo = args[0] - - try: - session.deleteRPMSig(rpminfo, sigkey=options.sigkey, all_sigs=options.all) - except koji.GenericError as e: - msg = str(e) - if msg.startswith("No such rpm"): - # make this a little more readable than the hub error - error("No such rpm in system: %s" % rpminfo) - else: - error("Signature removal failed: %s" % msg) - - -def handle_write_signed_rpm(goptions, session, args): - "[admin] Write signed RPMs to disk" - usage = "usage: %prog write-signed-rpm [options] [ ...]" - parser = OptionParser(usage=get_usage_str(usage)) - parser.add_option("--all", action="store_true", help="Write out all RPMs signed with this key") - parser.add_option("--buildid", help="Specify a build id rather than an n-v-r") - (options, args) = parser.parse_args(args) - if len(args) < 1: - parser.error("A signature key must be specified") - if len(args) < 2 and not (options.all or options.buildid): - parser.error("At least one RPM must be specified") - key = args.pop(0).lower() - activate_session(session, goptions) - if options.all: - rpms = session.queryRPMSigs(sigkey=key) - with session.multicall() as m: - results = [m.getRPM(r['rpm_id']) for r in rpms] - rpms = [x.result for x in results] - elif options.buildid: - rpms = session.listRPMs(int(options.buildid)) - else: - nvrs = [] - rpms = [] - - with session.multicall() as m: - result = [m.getRPM(nvra, strict=False) for nvra in args] - for rpm, nvra in zip(result, args): - rpm = rpm.result - if rpm: - rpms.append(rpm) - else: - nvrs.append(nvra) - - # for historical reasons, we also accept nvrs - with session.multicall() as m: - result = [m.getBuild(nvr, strict=True) for nvr in nvrs] - builds = [] - for nvr, build in zip(nvrs, result): - try: - builds.append(build.result['id']) - except koji.GenericError: - raise koji.GenericError("No such rpm or build: %s" % nvr) - - with session.multicall() as m: - rpm_lists = [m.listRPMs(buildID=build_id) for build_id in builds] - for rpm_list in rpm_lists: - rpms.extend(rpm_list.result) - - with session.multicall(strict=True) as m: - for i, rpminfo in enumerate(rpms): - nvra = "%(name)s-%(version)s-%(release)s.%(arch)s" % rpminfo - print("[%d/%d] %s" % (i + 1, len(rpms), nvra)) - m.writeSignedRPM(rpminfo['id'], key) - - -def handle_prune_signed_copies(goptions, session, args): - "[admin] Prune signed copies" - usage = "usage: %prog prune-signed-copies [options]" - parser = OptionParser(usage=get_usage_str(usage)) - parser.add_option("-n", "--test", action="store_true", help="Test mode") - parser.add_option("-v", "--verbose", action="store_true", help="Be more verbose") - parser.add_option("--days", type="int", default=5, help="Timeout before clearing") - parser.add_option("-p", "--package", "--pkg", help="Limit to a single package") - parser.add_option("-b", "--build", help="Limit to a single build") - parser.add_option("-i", "--ignore-tag", action="append", default=[], - help="Ignore these tags when considering whether a build is/was latest") - parser.add_option("--ignore-tag-file", - help="File to read tag ignore patterns from") - parser.add_option("-r", "--protect-tag", action="append", default=[], - help="Do not prune signed copies from matching tags") - parser.add_option("--protect-tag-file", - help="File to read tag protect patterns from") - parser.add_option("--trashcan-tag", default="trashcan", help="Specify trashcan tag") - # Don't use local debug option, this one stays here for backward compatibility - # https://pagure.io/koji/issue/2084 - parser.add_option("--debug", action="store_true", default=goptions.debug, help=SUPPRESS_HELP) - (options, args) = parser.parse_args(args) - # different ideas/modes - # 1) remove all signed copies of builds that are not latest for some tag - # 2) remove signed copies when a 'better' signature is available - # 3) for a specified tag, remove all signed copies that are not latest (w/ inheritance) - # 4) for a specified tag, remove all signed copies (no inheritance) - # (but skip builds that are multiply tagged) - - # for now, we're just implementing mode #1 - # (with the modification that we check to see if the build was latest within - # the last N days) - if options.ignore_tag_file: - with open(options.ignore_tag_file) as fo: - options.ignore_tag.extend([line.strip() for line in fo.readlines()]) - if options.protect_tag_file: - with open(options.protect_tag_file) as fo: - options.protect_tag.extend([line.strip() for line in fo.readlines()]) - if options.debug: - options.verbose = True - cutoff_ts = time.time() - options.days * 24 * 3600 - if options.debug: - print("Cutoff date: %s" % time.asctime(time.localtime(cutoff_ts))) - activate_session(session, goptions) - if not options.build: - if options.verbose: - print("Getting builds...") - qopts = { - 'state': koji.BUILD_STATES['COMPLETE'], - 'queryOpts': { - 'limit': 50000, - 'offset': 0, - 'order': 'build_id', - } - } - if options.package: - pkginfo = session.getPackage(options.package) - qopts['packageID'] = pkginfo['id'] - builds = [] - while True: - chunk = [(b['nvr'], b) for b in session.listBuilds(**qopts)] - if not chunk: - break - builds.extend(chunk) - qopts['queryOpts']['offset'] += qopts['queryOpts']['limit'] - if options.verbose: - print("...got %i builds" % len(builds)) - builds.sort() - else: - # single build - binfo = session.getBuild(options.build) - if not binfo: - parser.error('No such build: %s' % options.build) - builds = [("%(name)s-%(version)s-%(release)s" % binfo, binfo)] - total_files = 0 - total_space = 0 - - def _histline(event_id, x): - if event_id == x['revoke_event']: - ts = x['revoke_ts'] - fmt = "Untagged %(name)s-%(version)s-%(release)s from %(tag_name)s" - elif event_id == x['create_event']: - ts = x['create_ts'] - fmt = "Tagged %(name)s-%(version)s-%(release)s with %(tag_name)s" - if x['active']: - fmt += " [still active]" - else: - raise koji.GenericError("No such event: (%r, %r)" % (event_id, x)) - time_str = time.asctime(time.localtime(ts)) - return "%s: %s" % (time_str, fmt % x) - for nvr, binfo in builds: - # listBuilds returns slightly different data than normal - if 'id' not in binfo: - binfo['id'] = binfo['build_id'] - if 'name' not in binfo: - binfo['name'] = binfo['package_name'] - if options.debug: - print("DEBUG: %s" % nvr) - # see how recently this build was latest for a tag - is_latest = False - is_protected = False - last_latest = None - tags = {} - for entry in session.queryHistory(build=binfo['id'])['tag_listing']: - # we used queryHistory rather than listTags so we can consider tags - # that the build was recently untagged from - tags.setdefault(entry['tag.name'], 1) - if options.debug: - print("Tags: %s" % to_list(tags.keys())) - for tag_name in tags: - if tag_name == options.trashcan_tag: - if options.debug: - print("Ignoring trashcan tag for build %s" % nvr) - continue - ignore_tag = False - for pattern in options.ignore_tag: - if fnmatch.fnmatch(tag_name, pattern): - if options.debug: - print("Ignoring tag %s for build %s" % (tag_name, nvr)) - ignore_tag = True - break - if ignore_tag: - continue - # in order to determine how recently this build was latest, we have - # to look at the tagging history. - hist = session.queryHistory(tag=tag_name, package=binfo['name'])['tag_listing'] - if not hist: - # really shouldn't happen - raise koji.GenericError("No history found for %s in %s" % (nvr, tag_name)) - timeline = [] - for x in hist: - # note that for revoked entries, we're effectively splitting them into - # two parts: creation and revocation. - timeline.append((x['create_event'], 1, x)) - # at the same event, revokes happen first - if x['revoke_event'] is not None: - timeline.append((x['revoke_event'], 0, x)) - timeline.sort(key=lambda entry: entry[:2]) - # find most recent creation entry for our build and crop there - latest_ts = None - for i in range(len(timeline) - 1, -1, -1): - # searching in reverse cronological order - event_id, is_create, entry = timeline[i] - if entry['build_id'] == binfo['id'] and is_create: - latest_ts = event_id - break - if not latest_ts: - # really shouldn't happen - raise koji.GenericError("No creation event found for %s in %s" % (nvr, tag_name)) - our_entry = entry - if options.debug: - print(_histline(event_id, our_entry)) - # now go through the events since most recent creation entry - timeline = timeline[i + 1:] - if not timeline: - is_latest = True - if options.debug: - print("%s is latest in tag %s" % (nvr, tag_name)) - break - # before we go any further, is this a protected tag? - protect_tag = False - for pattern in options.protect_tag: - if fnmatch.fnmatch(tag_name, pattern): - protect_tag = True - break - if protect_tag: - # we use the same time limit as for the latest calculation - # if this build was in this tag within that limit, then we will - # not prune its signed copies - if our_entry['revoke_event'] is None: - # we're still tagged with a protected tag - if options.debug: - print("Build %s has protected tag %s" % (nvr, tag_name)) - is_protected = True - break - elif our_entry['revoke_ts'] > cutoff_ts: - # we were still tagged here sometime before the cutoff - if options.debug: - print("Build %s had protected tag %s until %s" - % (nvr, tag_name, - time.asctime(time.localtime(our_entry['revoke_ts'])))) - is_protected = True - break - replaced_ts = None - revoke_ts = None - others = {} - for event_id, is_create, entry in timeline: - # So two things can knock this build from the title of latest: - # - it could be untagged (entry revoked) - # - another build could become latest (replaced) - # Note however that if the superceding entry is itself revoked, then - # our build could become latest again - if options.debug: - print(_histline(event_id, entry)) - if entry['build_id'] == binfo['id']: - if is_create: - # shouldn't happen - raise koji.GenericError("Duplicate creation event found for %s in %s" - % (nvr, tag_name)) - else: - # we've been revoked - revoke_ts = entry['revoke_ts'] - break - else: - if is_create: - # this build has become latest - replaced_ts = entry['create_ts'] - if entry['active']: - # this entry not revoked yet, so we're done for this tag - break - # since this entry is revoked later, our build might eventually be - # uncovered, so we have to keep looking - others[entry['build_id']] = 1 - else: - # other build revoked - # see if our build has resurfaced - if entry['build_id'] in others: - del others[entry['build_id']] - if replaced_ts is not None and not others: - # we've become latest again - # (note: we're not revoked yet because that triggers a break above) - replaced_ts = None - latest_ts = entry['revoke_ts'] - if last_latest is None: - timestamps = [] - else: - timestamps = [last_latest] - if revoke_ts is None: - if replaced_ts is None: - # turns out we are still latest - is_latest = True - if options.debug: - print("%s is latest (again) in tag %s" % (nvr, tag_name)) - break - else: - # replaced (but not revoked) - timestamps.append(replaced_ts) - if options.debug: - print("tag %s: %s not latest (replaced %s)" - % (tag_name, nvr, time.asctime(time.localtime(replaced_ts)))) - elif replaced_ts is None: - # revoked but not replaced - timestamps.append(revoke_ts) - if options.debug: - print("tag %s: %s not latest (revoked %s)" - % (tag_name, nvr, time.asctime(time.localtime(revoke_ts)))) - else: - # revoked AND replaced - timestamps.append(min(revoke_ts, replaced_ts)) - if options.debug: - print("tag %s: %s not latest (revoked %s, replaced %s)" - % (tag_name, nvr, time.asctime(time.localtime(revoke_ts)), - time.asctime(time.localtime(replaced_ts)))) - last_latest = max(timestamps) - if last_latest > cutoff_ts: - if options.debug: - print("%s was latest past the cutoff" % nvr) - is_latest = True - break - if is_latest: - continue - if is_protected: - continue - # not latest anywhere since cutoff, so we can remove all signed copies - rpms = session.listRPMs(buildID=binfo['id']) - session.multicall = True - for rpminfo in rpms: - session.queryRPMSigs(rpm_id=rpminfo['id']) - by_sig = {} - # index by sig - for rpminfo, [sigs] in zip(rpms, session.multiCall()): - for sig in sigs: - sigkey = sig['sigkey'] - by_sig.setdefault(sigkey, []).append(rpminfo) - builddir = koji.pathinfo.build(binfo) - build_files = 0 - build_space = 0 - if not by_sig and options.debug: - print("(build has no signatures)") - for sigkey, rpms in six.iteritems(by_sig): - mycount = 0 - archdirs = {} - sigdirs = {} - for rpminfo in rpms: - signedpath = "%s/%s" % (builddir, koji.pathinfo.signed(rpminfo, sigkey)) - try: - st = os.lstat(signedpath) - except OSError: - continue - if not stat.S_ISREG(st.st_mode): - # warn about this - print("Skipping %s. Not a regular file" % signedpath) - continue - if st.st_mtime > cutoff_ts: - print("Skipping %s. File newer than cutoff" % signedpath) - continue - if options.test: - print("Would have unlinked: %s" % signedpath) - else: - if options.verbose: - print("Unlinking: %s" % signedpath) - try: - os.unlink(signedpath) - except OSError as e: - print("Error removing %s: %s" % (signedpath, e)) - print("This script needs write access to %s" % koji.BASEDIR) - continue - mycount += 1 - build_files += 1 - build_space += st.st_size - # XXX - this makes some layout assumptions, but - # pathinfo doesn't report what we need - mydir = os.path.dirname(signedpath) - archdirs[mydir] = 1 - sigdirs[os.path.dirname(mydir)] = 1 - for dir in archdirs: - if options.test: - print("Would have removed dir: %s" % dir) - else: - if options.verbose: - print("Removing dir: %s" % dir) - try: - os.rmdir(dir) - except OSError as e: - print("Error removing %s: %s" % (signedpath, e)) - if len(sigdirs) == 1: - dir = to_list(sigdirs.keys())[0] - if options.test: - print("Would have removed dir: %s" % dir) - else: - if options.verbose: - print("Removing dir: %s" % dir) - try: - os.rmdir(dir) - except OSError as e: - print("Error removing %s: %s" % (signedpath, e)) - elif len(sigdirs) > 1: - warn("More than one signature dir for %s: %r" % (sigkey, sigdirs)) - if build_files: - total_files += build_files - total_space += build_space - if options.verbose: - print("Build: %s, Removed %i signed copies (%i bytes). Total: %i/%i" - % (nvr, build_files, build_space, total_files, total_space)) - elif options.debug and by_sig: - print("(build has no signed copies)") - print("--- Grand Totals ---") - print("Files: %i" % total_files) - print("Bytes: %i" % total_space) - - -def handle_set_build_volume(goptions, session, args): - "[admin] Move a build to a different volume" - usage = "usage: %prog set-build-volume [ ...]" - parser = OptionParser(usage=get_usage_str(usage)) - parser.add_option("-v", "--verbose", action="store_true", help="Be verbose") - (options, args) = parser.parse_args(args) - if len(args) < 2: - parser.error("You must provide a volume and at least one build") - volinfo = session.getVolume(args[0]) - if not volinfo: - error("No such volume: %s" % args[0]) - activate_session(session, goptions) - builds = [] - for nvr in args[1:]: - binfo = session.getBuild(nvr) - if not binfo: - print("No such build: %s" % nvr) - elif binfo['volume_id'] == volinfo['id']: - print("Build %s already on volume %s" % (nvr, volinfo['name'])) - else: - builds.append(binfo) - if not builds: - error("No builds to move") - for binfo in builds: - session.changeBuildVolume(binfo['id'], volinfo['id']) - if options.verbose: - print("%s: %s -> %s" % (binfo['nvr'], binfo['volume_name'], volinfo['name'])) - - -def handle_add_volume(goptions, session, args): - "[admin] Add a new storage volume" - usage = "usage: %prog add-volume " - parser = OptionParser(usage=get_usage_str(usage)) - (options, args) = parser.parse_args(args) - if len(args) != 1: - parser.error("Command requires exactly one volume-name.") - name = args[0] - volinfo = session.getVolume(name) - if volinfo: - error("Volume %s already exists" % name) - activate_session(session, goptions) - volinfo = session.addVolume(name) - print("Added volume %(name)s with id %(id)i" % volinfo) - - -def anon_handle_list_volumes(goptions, session, args): - "[info] List storage volumes" - usage = "usage: %prog list-volumes" - parser = OptionParser(usage=get_usage_str(usage)) - (options, args) = parser.parse_args(args) - ensure_connection(session, goptions) - for volinfo in session.listVolumes(): - print(volinfo['name']) - - -def handle_list_permissions(goptions, session, args): - "[info] List user permissions" - usage = "usage: %prog list-permissions [options]" - parser = OptionParser(usage=get_usage_str(usage)) - parser.add_option("--user", help="List permissions for the given user") - parser.add_option("--mine", action="store_true", help="List your permissions") - parser.add_option("--quiet", action="store_true", default=goptions.quiet, - help="Do not print the header information") - (options, args) = parser.parse_args(args) - if len(args) > 0: - parser.error("This command takes no arguments") - activate_session(session, goptions) - perms = [] - if options.user: - user = session.getUser(options.user) - if not user: - error("No such user: %s" % options.user) - for p in session.getUserPerms(user['id']): - perms.append({'name': p}) - elif options.mine: - for p in session.getPerms(): - perms.append({'name': p}) - else: - for p in session.getAllPerms(): - perms.append({'name': p['name'], 'description': p['description']}) - if perms: - longest_perm = max([len(perm['name']) for perm in perms]) - perms = sorted(perms, key=lambda x: x['name']) - else: - longest_perm = 8 - if longest_perm < len('Permission name '): - longest_perm = len('Permission name ') - if not options.quiet: - hdr = '{permname:<{longest_perm}}' - hdr = hdr.format(longest_perm=longest_perm, permname='Permission name') - if perms and perms[0].get('description'): - hdr += " Description".ljust(53) - print(hdr) - print(len(hdr) * '-') - for perm in perms: - line = '{permname:<{longest_perm}}' - line = line.format(longest_perm=longest_perm, permname=perm['name']) - if perm.get('description'): - line += " %s" % perm['description'] - print(line) - - -def handle_add_user(goptions, session, args): - "[admin] Add a user" - usage = "usage: %prog add-user [options]" - parser = OptionParser(usage=get_usage_str(usage)) - parser.add_option("--principal", help="The Kerberos principal for this user") - parser.add_option("--disable", help="Prohibit logins by this user", action="store_true") - (options, args) = parser.parse_args(args) - if len(args) < 1: - parser.error("You must specify the username of the user to add") - elif len(args) > 1: - parser.error("This command only accepts one argument (username)") - username = args[0] - if options.disable: - status = koji.USER_STATUS['BLOCKED'] - else: - status = koji.USER_STATUS['NORMAL'] - activate_session(session, goptions) - user_id = session.createUser(username, status=status, krb_principal=options.principal) - print("Added user %s (%i)" % (username, user_id)) - - -def handle_enable_user(goptions, session, args): - "[admin] Enable logins by a user" - usage = "usage: %prog enable-user " - parser = OptionParser(usage=get_usage_str(usage)) - (options, args) = parser.parse_args(args) - if len(args) < 1: - parser.error("You must specify the username of the user to enable") - elif len(args) > 1: - parser.error("This command only accepts one argument (username)") - username = args[0] - activate_session(session, goptions) - session.enableUser(username) - - -def handle_disable_user(goptions, session, args): - "[admin] Disable logins by a user" - usage = "usage: %prog disable-user " - parser = OptionParser(usage=get_usage_str(usage)) - (options, args) = parser.parse_args(args) - if len(args) < 1: - parser.error("You must specify the username of the user to disable") - elif len(args) > 1: - parser.error("This command only accepts one argument (username)") - username = args[0] - activate_session(session, goptions) - session.disableUser(username) - - -def handle_edit_user(goptions, session, args): - "[admin] Alter user information" - usage = "usage: %prog edit-user [options]" - parser = OptionParser(usage=get_usage_str(usage)) - parser.add_option("--rename", help="Rename the user") - parser.add_option("--edit-krb", action="append", default=[], metavar="OLD=NEW", - help="Change kerberos principal of the user") - parser.add_option("--add-krb", action="append", default=[], metavar="KRB", - help="Add kerberos principal of the user") - parser.add_option("--remove-krb", action="append", default=[], metavar="KRB", - help="Remove kerberos principal of the user") - (options, args) = parser.parse_args(args) - if len(args) < 1: - parser.error("You must specify the username of the user to edit") - elif len(args) > 1: - parser.error("This command only accepts one argument (username)") - activate_session(session, goptions) - user = args[0] - princ_mappings = [] - for p in options.edit_krb: - old, new = p.split('=', 1) - princ_mappings.append({'old': arg_filter(old), 'new': arg_filter(new)}) - for a in options.add_krb: - princ_mappings.append({'old': None, 'new': arg_filter(a)}) - for r in options.remove_krb: - princ_mappings.append({'old': arg_filter(r), 'new': None}) - session.editUser(user, options.rename, princ_mappings) - - -def handle_list_signed(goptions, session, args): - "[admin] List signed copies of rpms" - usage = "usage: %prog list-signed [options]" - description = "You must have local access to Koji's topdir filesystem." - parser = OptionParser(usage=get_usage_str(usage), description=description) - # Don't use local debug option, this one stays here for backward compatibility - # https://pagure.io/koji/issue/2084 - parser.add_option("--debug", action="store_true", default=goptions.debug, help=SUPPRESS_HELP) - parser.add_option("--key", help="Only list RPMs signed with this key") - parser.add_option("--build", help="Only list RPMs from this build") - parser.add_option("--rpm", help="Only list signed copies for this RPM") - parser.add_option("--tag", help="Only list RPMs within this tag") - (options, args) = parser.parse_args(args) - if not options.build and not options.tag and not options.rpm: - parser.error("At least one from --build, --rpm, --tag needs to be specified.") - activate_session(session, goptions) - qopts = {} - build_idx = {} - rpm_idx = {} - if options.key: - qopts['sigkey'] = options.key - - sigs = [] - if options.rpm: - rpm_info = options.rpm - try: - rpm_info = int(rpm_info) - except ValueError: - pass - rinfo = session.getRPM(rpm_info, strict=True) - rpm_idx[rinfo['id']] = rinfo - if rinfo.get('external_repo_id'): - parser.error("External rpm: %(name)s-%(version)s-%(release)s.%(arch)s@" - "%(external_repo_name)s" % rinfo) - sigs += session.queryRPMSigs(rpm_id=rinfo['id'], **qopts) - if options.build: - build = options.build - try: - build = int(build) - except ValueError: - pass - binfo = session.getBuild(build, strict=True) - build_idx[binfo['id']] = binfo - rpms = session.listRPMs(buildID=binfo['id']) - for rinfo in rpms: - rpm_idx[rinfo['id']] = rinfo - sigs += session.queryRPMSigs(rpm_id=rinfo['id'], **qopts) - if options.tag: - tag = options.tag - try: - tag = int(tag) - except ValueError: - pass - rpms, builds = session.listTaggedRPMS(tag, inherit=False, latest=False) - tagged = {} - for binfo in builds: - build_idx.setdefault(binfo['id'], binfo) - results = [] - # use batched multicall as there could be potentially a lot of results - # so we don't exhaust server resources - with session.multicall(batch=5000) as m: - for rinfo in rpms: - rpm_idx.setdefault(rinfo['id'], rinfo) - tagged[rinfo['id']] = 1 - results.append(m.queryRPMSigs(rpm_id=rinfo['id']), **qopts) - sigs += [x.result[0] for x in results] - - # Now figure out which sig entries actually have live copies - for sig in sigs: - rpm_id = sig['rpm_id'] - sigkey = sig['sigkey'] - if options.tag: - if tagged.get(rpm_id) is None: - continue - rinfo = rpm_idx.get(rpm_id) - if not rinfo: - rinfo = session.getRPM(rpm_id) - rpm_idx[rinfo['id']] = rinfo - binfo = build_idx.get(rinfo['build_id']) - if not binfo: - binfo = session.getBuild(rinfo['build_id']) - build_idx[binfo['id']] = binfo - binfo['name'] = binfo['package_name'] - builddir = koji.pathinfo.build(binfo) - signedpath = "%s/%s" % (builddir, koji.pathinfo.signed(rinfo, sigkey)) - if not os.path.exists(signedpath): - if goptions.debug: - print("No copy: %s" % signedpath) - continue - print(signedpath) - - -def handle_import_archive(options, session, args): - "[admin] Import an archive file and associate it with a build" - usage = "usage: %prog import-archive [ 1: - parser.error("You must specify a build ID or N-V-R and an archive to import") - - activate_session(session, options) - - if not suboptions.type: - parser.error("You must specify an archive type") - if suboptions.type == 'maven': - if not (session.hasPerm('maven-import') or session.hasPerm('admin')): - parser.error("This action requires the maven-import privilege") - if not suboptions.type_info: - parser.error("--type-info must point to a .pom file when importing Maven archives") - pom_info = koji.parse_pom(suboptions.type_info) - maven_info = koji.pom_to_maven_info(pom_info) - suboptions.type_info = maven_info - elif suboptions.type == 'win': - if not (session.hasPerm('win-import') or session.hasPerm('admin')): - parser.error("This action requires the win-import privilege") - if not suboptions.type_info: - parser.error("--type-info must be specified") - type_info = suboptions.type_info.split(':', 2) - if len(type_info) < 2: - parser.error("--type-info must be in relpath:platforms[:flags] format") - win_info = {'relpath': type_info[0], 'platforms': type_info[1].split()} - if len(type_info) > 2: - win_info['flags'] = type_info[2].split() - else: - win_info['flags'] = [] - suboptions.type_info = win_info - elif suboptions.type == 'image': - if not (session.hasPerm('image-import') or session.hasPerm('admin')): - parser.error("This action requires the image-import privilege") - if not suboptions.type_info: - parser.error("--type-info must be specified") - image_info = {'arch': suboptions.type_info} - suboptions.type_info = image_info - else: - parser.error("Unsupported archive type: %s" % suboptions.type) - - buildinfo = session.getBuild(arg_filter(args[0])) - if not buildinfo: - if not suboptions.create_build: - parser.error("No such build: %s" % args[0]) - buildinfo = koji.parse_NVR(args[0]) - if buildinfo['epoch'] == '': - buildinfo['epoch'] = None - else: - buildinfo['epoch'] = int(buildinfo['epoch']) - if suboptions.type == 'maven': - # --type-info should point to a local .pom file - session.createMavenBuild(buildinfo, suboptions.type_info) - elif suboptions.type == 'win': - # We're importing, so we don't know what platform the build - # was run on. Use "import" as a placeholder. - session.createWinBuild(buildinfo, {'platform': 'import'}) - elif suboptions.type == 'image': - # --type-info should have an arch of the image - session.createImageBuild(buildinfo) - else: - # should get caught above - assert False # pragma: no cover - - for filepath in args[1:]: - filename = os.path.basename(filepath) - print("Uploading archive: %s" % filename) - serverdir = unique_path('cli-import') - if _running_in_bg() or suboptions.noprogress: - callback = None - else: - callback = _progress_callback - if suboptions.link: - linked_upload(filepath, serverdir) - else: - session.uploadWrapper(filepath, serverdir, callback=callback) - print('') - serverpath = "%s/%s" % (serverdir, filename) - session.importArchive(serverpath, buildinfo, suboptions.type, suboptions.type_info) - print("Imported: %s" % filename) - - -def handle_grant_permission(goptions, session, args): - "[admin] Grant a permission to a user" - usage = "usage: %prog grant-permission [options] [ ...]" - parser = OptionParser(usage=get_usage_str(usage)) - parser.add_option("--new", action="store_true", - help="Create this permission if the permission does not exist") - parser.add_option("--description", - help="Add description about new permission") - (options, args) = parser.parse_args(args) - if len(args) < 2: - parser.error("Please specify a permission and at least one user") - activate_session(session, goptions) - perm = args[0] - names = args[1:] - users = [] - for n in names: - user = session.getUser(n) - if user is None: - parser.error("No such user: %s" % n) - users.append(user) - kwargs = {} - if options.new: - kwargs['create'] = True - if options.description: - kwargs['description'] = options.description - if options.description and not options.new: - parser.error("Option new must be specified with option description.") - for user in users: - session.grantPermission(user['name'], perm, **kwargs) - - -def handle_revoke_permission(goptions, session, args): - "[admin] Revoke a permission from a user" - usage = "usage: %prog revoke-permission [ ...]" - parser = OptionParser(usage=get_usage_str(usage)) - (options, args) = parser.parse_args(args) - if len(args) < 2: - parser.error("Please specify a permission and at least one user") - activate_session(session, goptions) - perm = args[0] - names = args[1:] - users = [] - for n in names: - user = session.getUser(n) - if user is None: - parser.error("No such user: %s" % n) - users.append(user) - for user in users: - session.revokePermission(user['name'], perm) - - -def handle_edit_permission(goptions, session, args): - "[admin] Edit a permission description" - usage = "usage: %prog edit-permission " - parser = OptionParser(usage=get_usage_str(usage)) - (options, args) = parser.parse_args(args) - if len(args) < 2: - parser.error("Please specify a permission and a description") - activate_session(session, goptions) - perm = args[0] - description = args[1] - session.editPermission(perm, description) - - -def handle_grant_cg_access(goptions, session, args): - "[admin] Add a user to a content generator" - usage = "usage: %prog grant-cg-access " - parser = OptionParser(usage=get_usage_str(usage)) - parser.add_option("--new", action="store_true", help="Create a new content generator") - (options, args) = parser.parse_args(args) - if len(args) != 2: - parser.error("Please specify a user and content generator") - activate_session(session, goptions) - user = args[0] - cg = args[1] - uinfo = session.getUser(user) - if uinfo is None: - parser.error("No such user: %s" % user) - kwargs = {} - if options.new: - kwargs['create'] = True - session.grantCGAccess(uinfo['name'], cg, **kwargs) - - -def handle_revoke_cg_access(goptions, session, args): - "[admin] Remove a user from a content generator" - usage = "usage: %prog revoke-cg-access " - parser = OptionParser(usage=get_usage_str(usage)) - (options, args) = parser.parse_args(args) - if len(args) != 2: - parser.error("Please specify a user and content generator") - activate_session(session, goptions) - user = args[0] - cg = args[1] - uinfo = session.getUser(user) - if uinfo is None: - parser.error("No such user: %s" % user) - session.revokeCGAccess(uinfo['name'], cg) - - -def anon_handle_latest_build(goptions, session, args): - """[info] Print the latest builds for a tag""" - usage = """\ - usage: %prog latest-build [options] [ ...] - - The first option should be the name of a tag, not the name of a build target. - If you want to know the latest build in buildroots for a given build target, - then you should use the name of the build tag for that target. You can find - this value by running '%prog list-targets --name=' - - More information on tags and build targets can be found in the documentation. - https://docs.pagure.org/koji/HOWTO/#package-organization""" - - usage = textwrap.dedent(usage) - parser = OptionParser(usage=get_usage_str(usage)) - parser.add_option("--arch", help="List all of the latest packages for this arch") - parser.add_option("--all", action="store_true", - help="List all of the latest packages for this tag") - parser.add_option("--quiet", action="store_true", default=goptions.quiet, - help="Do not print the header information") - parser.add_option("--paths", action="store_true", help="Show the file paths") - parser.add_option("--type", - help="Show builds of the given type only. " - "Currently supported types: maven, win, image, or any custom " - "content generator btypes") - (options, args) = parser.parse_args(args) - if len(args) == 0: - parser.error("A tag name must be specified") - ensure_connection(session, goptions) - if options.all: - if len(args) > 1: - parser.error("A package name may not be combined with --all") - # Set None as the package argument - args.append(None) - else: - if len(args) < 2: - parser.error("A tag name and package name must be specified") - pathinfo = koji.PathInfo() - - for pkg in args[1:]: - if options.arch: - rpms, builds = session.getLatestRPMS(args[0], package=pkg, arch=options.arch) - builds_hash = dict([(x['build_id'], x) for x in builds]) - data = rpms - if options.paths: - for x in data: - z = x.copy() - x['name'] = builds_hash[x['build_id']]['package_name'] - x['path'] = os.path.join(pathinfo.build(x), pathinfo.rpm(z)) - fmt = "%(path)s" - else: - fmt = "%(name)s-%(version)s-%(release)s.%(arch)s" - else: - kwargs = {'package': pkg} - if options.type: - kwargs['type'] = options.type - data = session.getLatestBuilds(args[0], **kwargs) - if options.paths: - if options.type == 'maven': - for x in data: - x['path'] = pathinfo.mavenbuild(x) - fmt = "%(path)-40s %(tag_name)-20s %(maven_group_id)-20s " \ - "%(maven_artifact_id)-20s %(owner_name)s" - else: - for x in data: - x['path'] = pathinfo.build(x) - fmt = "%(path)-40s %(tag_name)-20s %(owner_name)s" - else: - if options.type == 'maven': - fmt = "%(nvr)-40s %(tag_name)-20s %(maven_group_id)-20s " \ - "%(maven_artifact_id)-20s %(owner_name)s" - else: - fmt = "%(nvr)-40s %(tag_name)-20s %(owner_name)s" - if not options.quiet: - if options.type == 'maven': - print("%-40s %-20s %-20s %-20s %s" % - ("Build", "Tag", "Group Id", "Artifact Id", "Built by")) - print("%s %s %s %s %s" % - ("-" * 40, "-" * 20, "-" * 20, "-" * 20, "-" * 16)) - else: - print("%-40s %-20s %s" % ("Build", "Tag", "Built by")) - print("%s %s %s" % ("-" * 40, "-" * 20, "-" * 16)) - options.quiet = True - - output = sorted([fmt % x for x in data]) - for line in output: - print(line) - - -def anon_handle_list_api(goptions, session, args): - "[info] Print the list of XML-RPC APIs" - usage = "usage: %prog list-api [options] [method_name ...]" - parser = OptionParser(usage=get_usage_str(usage)) - (options, args) = parser.parse_args(args) - ensure_connection(session, goptions) - if args: - for method in args: - help = session.system.methodHelp(method) - if not help: - parser.error("Unknown method: %s" % method) - print(help) - else: - for x in sorted(session._listapi(), key=lambda x: x['name']): - if 'argdesc' in x: - args = x['argdesc'] - elif x['args']: - # older servers may not provide argdesc - expanded = [] - for arg in x['args']: - if isinstance(arg, str): - expanded.append(arg) - else: - expanded.append('%s=%s' % (arg[0], arg[1])) - args = "(%s)" % ", ".join(expanded) - else: - args = "()" - print('%s%s' % (x['name'], args)) - if x['doc']: - print(" description: %s" % x['doc']) - - -def anon_handle_list_tagged(goptions, session, args): - "[info] List the builds or rpms in a tag" - usage = "usage: %prog list-tagged [options] []" - parser = OptionParser(usage=get_usage_str(usage)) - parser.add_option("--arch", action="append", default=[], help="List rpms for this arch") - parser.add_option("--rpms", action="store_true", help="Show rpms instead of builds") - parser.add_option("--inherit", action="store_true", help="Follow inheritance") - parser.add_option("--latest", action="store_true", help="Only show the latest builds/rpms") - parser.add_option("--latest-n", type='int', metavar="N", - help="Only show the latest N builds/rpms") - parser.add_option("--quiet", action="store_true", default=goptions.quiet, - help="Do not print the header information") - parser.add_option("--paths", action="store_true", help="Show the file paths") - parser.add_option("--sigs", action="store_true", help="Show signatures") - parser.add_option("--type", - help="Show builds of the given type only. " - "Currently supported types: maven, win, image") - parser.add_option("--event", type='int', metavar="EVENT#", help="query at event") - parser.add_option("--ts", type='int', metavar="TIMESTAMP", - help="query at last event before timestamp") - parser.add_option("--repo", type='int', metavar="REPO#", help="query at event for a repo") - (options, args) = parser.parse_args(args) - if len(args) == 0: - parser.error("A tag name must be specified") - elif len(args) > 2: - parser.error("Only one package name may be specified") - ensure_connection(session, goptions) - pathinfo = koji.PathInfo() - package = None - if len(args) > 1: - package = args[1] - tag = args[0] - opts = {} - for key in ('latest', 'inherit'): - opts[key] = getattr(options, key) - if options.latest_n is not None: - opts['latest'] = options.latest_n - if package: - opts['package'] = package - if options.arch: - options.rpms = True - opts['arch'] = options.arch - if options.sigs: - opts['rpmsigs'] = True - options.rpms = True - if options.type: - opts['type'] = options.type - event = koji.util.eventFromOpts(session, options) - event_id = None - if event: - opts['event'] = event['id'] - event_id = event['id'] - event['timestr'] = time.asctime(time.localtime(event['ts'])) - if not options.quiet: - print("Querying at event %(id)i (%(timestr)s)" % event) - - # check if tag exist(s|ed) - taginfo = session.getTag(tag, event=event_id) - if not taginfo: - parser.error("No such tag: %s" % tag) - - if options.rpms: - rpms, builds = session.listTaggedRPMS(tag, **opts) - data = rpms - if options.paths: - build_idx = dict([(b['id'], b) for b in builds]) - for rinfo in data: - build = build_idx[rinfo['build_id']] - builddir = pathinfo.build(build) - if options.sigs: - sigkey = rinfo['sigkey'] - signedpath = os.path.join(builddir, pathinfo.signed(rinfo, sigkey)) - if os.path.exists(signedpath): - rinfo['path'] = signedpath - else: - rinfo['path'] = os.path.join(builddir, pathinfo.rpm(rinfo)) - fmt = "%(path)s" - data = [x for x in data if 'path' in x] - else: - fmt = "%(name)s-%(version)s-%(release)s.%(arch)s" - if options.sigs: - fmt = "%(sigkey)s " + fmt - else: - data = session.listTagged(tag, **opts) - if options.paths: - if options.type == 'maven': - for x in data: - x['path'] = pathinfo.mavenbuild(x) - fmt = "%(path)-40s %(tag_name)-20s %(maven_group_id)-20s " \ - "%(maven_artifact_id)-20s %(owner_name)s" - else: - for x in data: - x['path'] = pathinfo.build(x) - fmt = "%(path)-40s %(tag_name)-20s %(owner_name)s" - else: - if options.type == 'maven': - fmt = "%(nvr)-40s %(tag_name)-20s %(maven_group_id)-20s " \ - "%(maven_artifact_id)-20s %(owner_name)s" - else: - fmt = "%(nvr)-40s %(tag_name)-20s %(owner_name)s" - if not options.quiet: - if options.type == 'maven': - print("%-40s %-20s %-20s %-20s %s" % - ("Build", "Tag", "Group Id", "Artifact Id", "Built by")) - print("%s %s %s %s %s" % - ("-" * 40, "-" * 20, "-" * 20, "-" * 20, "-" * 16)) - else: - print("%-40s %-20s %s" % ("Build", "Tag", "Built by")) - print("%s %s %s" % ("-" * 40, "-" * 20, "-" * 16)) - - output = sorted([fmt % x for x in data]) - for line in output: - print(line) - - -def anon_handle_list_buildroot(goptions, session, args): - "[info] List the rpms used in or built in a buildroot" - usage = "usage: %prog list-buildroot [options] " - parser = OptionParser(usage=get_usage_str(usage)) - parser.add_option("--paths", action="store_true", help=SUPPRESS_HELP) - parser.add_option("--built", action="store_true", help="Show the built rpms") - parser.add_option("--verbose", "-v", action="store_true", help="Show more information") - (options, args) = parser.parse_args(args) - if len(args) != 1: - parser.error("Incorrect number of arguments") - ensure_connection(session, goptions) - if options.paths: - parser.error("--paths option is deprecated and will be removed in 1.30") - buildrootID = int(args[0]) - opts = {} - if options.built: - opts['buildrootID'] = buildrootID - else: - opts['componentBuildrootID'] = buildrootID - data = session.listRPMs(**opts) - - fmt = "%(nvr)s.%(arch)s" - order = sorted([(fmt % x, x) for x in data]) - for nvra, rinfo in order: - if options.verbose and rinfo.get('is_update'): - print("%s [update]" % nvra) - else: - print(nvra) - - -def anon_handle_list_untagged(goptions, session, args): - "[info] List untagged builds" - usage = "usage: %prog list-untagged [options] []" - parser = OptionParser(usage=get_usage_str(usage)) - parser.add_option("--paths", action="store_true", help="Show the file paths") - parser.add_option("--show-references", action="store_true", help="Show build references") - (options, args) = parser.parse_args(args) - if len(args) > 1: - parser.error("Only one package name may be specified") - ensure_connection(session, goptions) - package = None - if len(args) > 0: - package = args[0] - opts = {} - if package: - package_id = session.getPackageID(package) - if package_id is None: - error("No such package: %s" % package) - opts['name'] = package - pathinfo = koji.PathInfo() - - data = session.untaggedBuilds(**opts) - if options.show_references: - print("(Showing build references)") - references = {} - with session.multicall(strict=True, batch=10000) as m: - for build in data: - references[build['id']] = m.buildReferences(build['id']) - - for build in data: - refs = references[build['id']].result - r = [] - if refs.get('rpms'): - r.append("rpms: %s" % refs['rpms']) - if refs.get('component_of'): - r.append("images/archives: %s" % refs['component_of']) - if refs.get('archives'): - r.append("archives buildroots: %s" % refs['archives']) - build['refs'] = ', '.join(r) - if options.paths: - for x in data: - x['path'] = pathinfo.build(x) - fmt = "%(path)s" - else: - fmt = "%(name)s-%(version)s-%(release)s" - if options.show_references: - fmt = fmt + " %(refs)s" - output = sorted([fmt % x for x in data]) - for line in output: - print(line) - - -def print_group_list_req_group(group): - print(" @%(name)s [%(tag_name)s]" % group) - - -def print_group_list_req_package(pkg): - fmt = " %(package)s: %(basearchonly)s, %(type)s [%(tag_name)s]" - if pkg['blocked']: - fmt += " [BLOCKED]" - print(fmt % pkg) - - -def anon_handle_list_groups(goptions, session, args): - "[info] Print the group listings" - usage = "usage: %prog list-groups [options] []" - parser = OptionParser(usage=get_usage_str(usage)) - parser.add_option("--event", type='int', metavar="EVENT#", help="query at event") - parser.add_option("--ts", type='int', metavar="TIMESTAMP", - help="query at last event before timestamp") - parser.add_option("--repo", type='int', metavar="REPO#", help="query at event for a repo") - parser.add_option("--show-blocked", action="store_true", dest="incl_blocked", - help="Show blocked packages and groups") - (options, args) = parser.parse_args(args) - if len(args) < 1 or len(args) > 2: - parser.error("Incorrect number of arguments") - opts = {} - if options.incl_blocked: - opts['incl_blocked'] = True - ensure_connection(session, goptions) - event = koji.util.eventFromOpts(session, options) - if event: - opts['event'] = event['id'] - event['timestr'] = time.asctime(time.localtime(event['ts'])) - print("Querying at event %(id)i (%(timestr)s)" % event) - tmp_list = sorted([(x['name'], x) for x in session.getTagGroups(args[0], **opts)]) - groups = [x[1] for x in tmp_list] - - tags_cache = {} - - def get_cached_tag(tag_id): - if tag_id not in tags_cache: - tag = session.getTag(tag_id, strict=False) - if tag is None: - tags_cache[tag_id] = tag_id - else: - tags_cache[tag_id] = tag['name'] - return tags_cache[tag_id] - - for group in groups: - if len(args) > 1 and group['name'] != args[1]: - continue - print("%s [%s]" % (group['name'], get_cached_tag(group['tag_id']))) - groups = sorted([(x['name'], x) for x in group['grouplist']]) - for x in [x[1] for x in groups]: - x['tag_name'] = get_cached_tag(x['tag_id']) - print_group_list_req_group(x) - pkgs = sorted([(x['package'], x) for x in group['packagelist']]) - for x in [x[1] for x in pkgs]: - x['tag_name'] = get_cached_tag(x['tag_id']) - print_group_list_req_package(x) - - -def handle_add_group_pkg(goptions, session, args): - "[admin] Add a package to a group's package listing" - usage = "usage: %prog add-group-pkg [options] [ ...]" - parser = OptionParser(usage=get_usage_str(usage)) - (options, args) = parser.parse_args(args) - if len(args) < 3: - parser.error("You must specify a tag name, group name, and one or more package names") - tag = args[0] - group = args[1] - activate_session(session, goptions) - for pkg in args[2:]: - session.groupPackageListAdd(tag, group, pkg) - - -def handle_block_group_pkg(goptions, session, args): - "[admin] Block a package from a group's package listing" - usage = "usage: %prog block-group-pkg [options] [ ...]" - usage += '\n' + "Note that blocking is propagated through the inheritance chain, so " \ - "it is not exactly the same as package removal." - parser = OptionParser(usage=get_usage_str(usage)) - (options, args) = parser.parse_args(args) - if len(args) < 3: - parser.error("You must specify a tag name, group name, and one or more package names") - tag = args[0] - group = args[1] - activate_session(session, goptions) - for pkg in args[2:]: - session.groupPackageListBlock(tag, group, pkg) - - -def handle_unblock_group_pkg(goptions, session, args): - "[admin] Unblock a package from a group's package listing" - usage = "usage: %prog unblock-group-pkg [options] [ ...]" - parser = OptionParser(usage=get_usage_str(usage)) - (options, args) = parser.parse_args(args) - if len(args) < 3: - parser.error("You must specify a tag name, group name, and one or more package names") - tag = args[0] - group = args[1] - activate_session(session, goptions) - for pkg in args[2:]: - session.groupPackageListUnblock(tag, group, pkg) - - -def handle_add_group_req(goptions, session, args): - "[admin] Add a group to a group's required list" - usage = "usage: %prog add-group-req [options] " - parser = OptionParser(usage=get_usage_str(usage)) - (options, args) = parser.parse_args(args) - if len(args) != 3: - parser.error("You must specify a tag name and two group names") - tag = args[0] - group = args[1] - req = args[2] - activate_session(session, goptions) - session.groupReqListAdd(tag, group, req) - - -def handle_block_group_req(goptions, session, args): - "[admin] Block a group's requirement listing" - usage = "usage: %prog block-group-req [options] " - parser = OptionParser(usage=get_usage_str(usage)) - (options, args) = parser.parse_args(args) - if len(args) != 3: - parser.error("You must specify a tag name and two group names") - tag = args[0] - group = args[1] - req = args[2] - activate_session(session, goptions) - session.groupReqListBlock(tag, group, req) - - -def handle_unblock_group_req(goptions, session, args): - "[admin] Unblock a group's requirement listing" - usage = "usage: %prog unblock-group-req [options] " - parser = OptionParser(usage=get_usage_str(usage)) - (options, args) = parser.parse_args(args) - if len(args) != 3: - parser.error("You must specify a tag name and two group names") - tag = args[0] - group = args[1] - req = args[2] - activate_session(session, goptions) - session.groupReqListUnblock(tag, group, req) - - -def anon_handle_list_channels(goptions, session, args): - "[info] Print channels listing" - usage = "usage: %prog list-channels [options]" - parser = OptionParser(usage=get_usage_str(usage)) - parser.add_option("--simple", action="store_true", default=False, - help="Print just list of channels without additional info") - parser.add_option("--quiet", action="store_true", default=goptions.quiet, - help="Do not print header information") - parser.add_option("--comment", action="store_true", help="Show comments") - parser.add_option("--description", action="store_true", help="Show descriptions") - parser.add_option("--enabled", action="store_true", help="Limit to enabled channels") - parser.add_option("--not-enabled", action="store_false", dest="enabled", - help="Limit to not enabled channels") - parser.add_option("--disabled", action="store_false", dest="enabled", - help="Alias for --not-enabled") - (options, args) = parser.parse_args(args) - ensure_connection(session, goptions) - opts = {} - if options.enabled is not None: - opts['enabled'] = options.enabled - try: - channels = sorted([x for x in session.listChannels(**opts)], key=lambda x: x['name']) - except koji.ParameterError: - channels = sorted([x for x in session.listChannels()], key=lambda x: x['name']) - if len(channels) > 0: - first_item = channels[0] - else: - first_item = {} - session.multicall = True - for channel in channels: - session.listHosts(channelID=channel['id']) - for channel, [hosts] in zip(channels, session.multiCall()): - channel['enabled_host'] = len([x for x in hosts if x['enabled']]) - channel['disabled'] = len(hosts) - channel['enabled_host'] - channel['ready'] = len([x for x in hosts if x['ready']]) - channel['capacity'] = sum([x['capacity'] for x in hosts]) - channel['load'] = sum([x['task_load'] for x in hosts]) - if 'comment' in first_item: - channel['comment'] = truncate_string(channel['comment']) - if 'description' in first_item: - channel['description'] = truncate_string(channel['description']) - if channel['capacity']: - channel['perc_load'] = channel['load'] / channel['capacity'] * 100.0 - else: - channel['perc_load'] = 0.0 - if 'enabled' in first_item: - if not channel['enabled']: - channel['name'] = channel['name'] + ' [disabled]' - if channels: - longest_channel = max([len(ch['name']) for ch in channels]) - else: - longest_channel = 8 - if options.simple: - if not options.quiet: - hdr = 'Channel' - print(hdr) - print(len(hdr) * '-') - for channel in channels: - print(channel['name']) - else: - if not options.quiet: - hdr = '{channame:<{longest_channel}}Enabled Ready Disbld Load Cap ' \ - 'Perc ' - hdr = hdr.format(longest_channel=longest_channel, channame='Channel') - if options.description and 'description' in first_item: - hdr += "Description".ljust(53) - if options.comment and 'comment' in first_item: - hdr += "Comment".ljust(53) - print(hdr) - print(len(hdr) * '-') - mask = "%%(name)-%ss %%(enabled_host)6d %%(ready)6d %%(disabled)6d %%(load)6d %%(" \ - "capacity)6d %%(perc_load)6d%%%%" % longest_channel - if options.description and 'description' in first_item: - mask += " %(description)-50s" - if options.comment and 'comment' in first_item: - mask += " %(comment)-50s" - for channel in channels: - print(mask % channel) - - -def anon_handle_list_hosts(goptions, session, args): - "[info] Print the host listing" - usage = "usage: %prog list-hosts [options]" - parser = OptionParser(usage=get_usage_str(usage)) - parser.add_option("--arch", action="append", default=[], help="Specify an architecture") - parser.add_option("--channel", help="Specify a channel") - parser.add_option("--ready", action="store_true", help="Limit to ready hosts") - parser.add_option("--not-ready", action="store_false", dest="ready", - help="Limit to not ready hosts") - parser.add_option("--enabled", action="store_true", help="Limit to enabled hosts") - parser.add_option("--not-enabled", action="store_false", dest="enabled", - help="Limit to not enabled hosts") - parser.add_option("--disabled", action="store_false", dest="enabled", - help="Alias for --not-enabled") - parser.add_option("--quiet", action="store_true", default=goptions.quiet, - help="Do not print header information") - parser.add_option("--show-channels", action="store_true", help="Show host's channels") - parser.add_option("--comment", action="store_true", help="Show comments") - parser.add_option("--description", action="store_true", help="Show descriptions") - (options, args) = parser.parse_args(args) - opts = {} - ensure_connection(session, goptions) - if options.arch: - opts['arches'] = options.arch - if options.channel: - channel = session.getChannel(options.channel) - if not channel: - parser.error('No such channel: %s' % options.channel) - opts['channelID'] = channel['id'] - if options.ready is not None: - opts['ready'] = options.ready - if options.enabled is not None: - opts['enabled'] = options.enabled - tmp_list = sorted([(x['name'], x) for x in session.listHosts(**opts)]) - hosts = [x[1] for x in tmp_list] - - if not hosts: - warn("No hosts found.") - return - - def yesno(x): - if x: - return 'Y' - else: - return 'N' - - try: - first = session.getLastHostUpdate(hosts[0]['id'], ts=True) - opts = {'ts': True} - except koji.ParameterError: - # Hubs prior to v1.25.0 do not have a "ts" parameter for getLastHostUpdate - first = session.getLastHostUpdate(hosts[0]['id']) - opts = {} - - # pull in the last update using multicall to speed it up a bit - with session.multicall() as m: - result = [m.getLastHostUpdate(host['id'], **opts) for host in hosts[1:]] - updateList = [first] + [x.result for x in result] - - for host, update in zip(hosts, updateList): - if update is None: - host['update'] = '-' - else: - host['update'] = koji.formatTimeLong(update) - host['enabled'] = yesno(host['enabled']) - host['ready'] = yesno(host['ready']) - host['arches'] = ','.join(host['arches'].split()) - host['description'] = truncate_string(host['description']) - host['comment'] = truncate_string(host['comment']) - - # pull hosts' channels - if options.show_channels: - with session.multicall() as m: - result = [m.listChannels(host['id']) for host in hosts] - first_line_channel = result[0].result[0] - for host, channels in zip(hosts, result): - list_channels = [] - for c in channels.result: - if 'enabled' in first_line_channel: - if c['enabled']: - list_channels.append(c['name']) - else: - list_channels.append('*' + c['name']) - else: - list_channels.append(c['name']) - host['channels'] = ','.join(sorted(list_channels)) - - if hosts: - longest_host = max([len(h['name']) for h in hosts]) - else: - longest_host = 8 - - if not options.quiet: - hdr = "{hostname:<{longest_host}} Enb Rdy Load/Cap Arches " \ - "Last Update " - hdr = hdr.format(longest_host=longest_host, hostname='Hostname') - if options.description: - hdr += "Description".ljust(51) - if options.comment: - hdr += "Comment".ljust(51) - if options.show_channels: - hdr += "Channels" - print(hdr) - print(len(hdr) * '-') - mask = "%%(name)-%ss %%(enabled)-3s %%(ready)-3s %%(task_load)4.1f/%%(capacity)-4.1f " \ - "%%(arches)-16s %%(update)-35s" % longest_host - if options.description: - mask += " %(description)-50s" - if options.comment: - mask += " %(comment)-50s" - if options.show_channels: - mask += " %(channels)s" - for host in hosts: - print(mask % host) - - -def anon_handle_list_pkgs(goptions, session, args): - "[info] Print the package listing for tag or for owner" - usage = "usage: %prog list-pkgs [options]" - parser = OptionParser(usage=get_usage_str(usage)) - parser.add_option("--owner", help="Specify owner") - parser.add_option("--tag", help="Specify tag") - parser.add_option("--package", help="Specify package") - parser.add_option("--quiet", action="store_true", default=goptions.quiet, - help="Do not print header information") - parser.add_option("--noinherit", action="store_true", help="Don't follow inheritance") - parser.add_option("--show-blocked", action="store_true", help="Show blocked packages") - parser.add_option("--show-dups", action="store_true", help="Show superseded owners") - parser.add_option("--event", type='int', metavar="EVENT#", help="query at event") - parser.add_option("--ts", type='int', metavar="TIMESTAMP", - help="query at last event before timestamp") - parser.add_option("--repo", type='int', metavar="REPO#", help="query at event for a repo") - (options, args) = parser.parse_args(args) - if len(args) != 0: - parser.error("This command takes no arguments") - ensure_connection(session, goptions) - opts = {} - if options.owner: - user = session.getUser(options.owner) - if user is None: - parser.error("No such user: %s" % options.owner) - opts['userID'] = user['id'] - if options.tag: - tag = session.getTag(options.tag) - if tag is None: - parser.error("No such tag: %s" % options.tag) - opts['tagID'] = tag['id'] - if options.package: - opts['pkgID'] = options.package - allpkgs = False - if not opts: - # no limiting clauses were specified - allpkgs = True - opts['inherited'] = not options.noinherit - # hiding dups only makes sense if we're querying a tag - if options.tag: - opts['with_dups'] = options.show_dups - else: - opts['with_dups'] = True - event = koji.util.eventFromOpts(session, options) - if event: - opts['event'] = event['id'] - event['timestr'] = time.asctime(time.localtime(event['ts'])) - print("Querying at event %(id)i (%(timestr)s)" % event) - - if not opts.get('tagID') and not opts.get('userID') and \ - not opts.get('pkgID'): - if opts.get('event'): - parser.error("--event and --ts makes sense only with --tag," - " --owner or --package") - if options.show_blocked: - parser.error("--show-blocked makes sense only with --tag," - " --owner or --package") - if options.show_blocked: - opts['with_blocked'] = options.show_blocked - - try: - data = session.listPackages(**opts) - except koji.ParameterError: - del opts['with_blocked'] - data = session.listPackages(**opts) - - if not data: - error("(no matching packages)") - if not options.quiet: - if allpkgs: - print("Package") - print('-' * 23) - else: - print("%-23s %-23s %-16s %-15s" % ('Package', 'Tag', 'Extra Arches', 'Owner')) - print("%s %s %s %s" % ('-' * 23, '-' * 23, '-' * 16, '-' * 15)) - for pkg in data: - if allpkgs: - print(pkg['package_name']) - else: - if not options.show_blocked and pkg.get('blocked', False): - continue - if 'tag_id' in pkg: - if pkg['extra_arches'] is None: - pkg['extra_arches'] = "" - fmt = "%(package_name)-23s %(tag_name)-23s %(extra_arches)-16s %(owner_name)-15s" - if pkg.get('blocked', False): - fmt += " [BLOCKED]" - else: - fmt = "%(package_name)s" - print(fmt % pkg) - - -def anon_handle_list_builds(goptions, session, args): - "[info] Print the build listing" - usage = "usage: %prog list-builds [options]" - parser = OptionParser(usage=get_usage_str(usage), option_class=TimeOption) - parser.add_option("--package", help="List builds for this package") - parser.add_option("--buildid", help="List specific build from ID or nvr") - parser.add_option("--before", type="time", - help="List builds built before this time, " + TimeOption.get_help()) - parser.add_option("--after", type="time", - help="List builds built after this time (same format as for --before") - parser.add_option("--state", help="List builds in this state") - parser.add_option("--task", help="List builds for this task") - parser.add_option("--type", help="List builds of this type.") - parser.add_option("--prefix", help="Only builds starting with this prefix") - parser.add_option("--pattern", help="Only list builds matching this GLOB pattern") - parser.add_option("--cg", help="Only list builds imported by matching content generator name") - parser.add_option("--source", help="Only builds where the source field matches (glob pattern)") - parser.add_option("--owner", help="List builds built by this owner") - parser.add_option("--volume", help="List builds by volume ID") - parser.add_option("-k", "--sort-key", action="append", metavar='FIELD', - default=[], help="Sort the list by the named field. Allowed sort keys: " - "nvr, owner_name, state") - parser.add_option("-r", "--reverse", action="store_true", default=False, - help="Print the list in reverse order") - parser.add_option("--quiet", action="store_true", default=goptions.quiet, - help="Do not print the header information") - (options, args) = parser.parse_args(args) - if len(args) != 0: - parser.error("This command takes no arguments") - ensure_connection(session, goptions) - opts = {} - for key in ('type', 'prefix', 'pattern'): - value = getattr(options, key) - if value is not None: - opts[key] = value - if options.cg: - opts['cgID'] = options.cg - if options.package: - try: - opts['packageID'] = int(options.package) - except ValueError: - package = session.getPackageID(options.package) - if package is None: - parser.error("No such package: %s" % options.package) - opts['packageID'] = package - if options.owner: - try: - opts['userID'] = int(options.owner) - except ValueError: - user = session.getUser(options.owner) - if user is None: - parser.error("No such user: %s" % options.owner) - opts['userID'] = user['id'] - if options.volume: - try: - opts['volumeID'] = int(options.volume) - except ValueError: - volumes = session.listVolumes() - volumeID = None - for volume in volumes: - if options.volume == volume['name']: - volumeID = volume['id'] - if volumeID is None: - parser.error("No such volume: %s" % options.volume) - opts['volumeID'] = volumeID - if options.state: - try: - state = int(options.state) - if state > 4 or state < 0: - parser.error("Invalid state: %s" % options.state) - opts['state'] = state - except ValueError: - try: - opts['state'] = koji.BUILD_STATES[options.state] - except KeyError: - parser.error("Invalid state: %s" % options.state) - if options.before: - opts['completeBefore'] = options.before - if options.after: - opts['completeAfter'] = options.after - if options.task: - try: - opts['taskID'] = int(options.task) - except ValueError: - parser.error("Task id must be an integer") - if options.source: - opts['source'] = options.source - if options.buildid: - try: - buildid = int(options.buildid) - except ValueError: - buildid = options.buildid - data = [session.getBuild(buildid)] - if data[0] is None: - parser.error("No such build: '%s'" % buildid) - else: - # Check filter exists - if any(opts): - try: - data = session.listBuilds(**opts) - except koji.ParameterError as e: - if e.args[0].endswith("'pattern'"): - parser.error("The hub doesn't support the 'pattern' argument, please try" - " filtering the result on your local instead.") - if e.args[0].endswith("'cgID'"): - parser.error("The hub doesn't support the 'cg' argument, please try" - " filtering the result on your local instead.") - else: - parser.error("Filter must be provided for list") - if not options.sort_key: - options.sort_key = ['nvr'] - else: - for s_key in options.sort_key: - if s_key not in ['nvr', 'owner_name', 'state']: - warn("Invalid sort_key: %s." % s_key) - - data = sorted(data, key=lambda b: [b.get(k) for k in options.sort_key], - reverse=options.reverse) - for build in data: - build['state'] = koji.BUILD_STATES[build['state']] - - fmt = "%(nvr)-55s %(owner_name)-16s %(state)s" - if not options.quiet: - print("%-55s %-16s %s" % ("Build", "Built by", "State")) - print("%s %s %s" % ("-" * 55, "-" * 16, "-" * 16)) - - for build in data: - print(fmt % build) - - -def anon_handle_rpminfo(goptions, session, args): - "[info] Print basic information about an RPM" - usage = "usage: %prog rpminfo [options] [ ...]" - parser = OptionParser(usage=get_usage_str(usage)) - parser.add_option("--buildroots", action="store_true", - help="show buildroots the rpm was used in") - (options, args) = parser.parse_args(args) - if len(args) < 1: - parser.error("Please specify an RPM") - ensure_connection(session, goptions) - error_hit = False - for rpm in args: - info = session.getRPM(rpm) - if info is None: - warn("No such rpm: %s\n" % rpm) - error_hit = True - continue - if info['epoch'] is None: - info['epoch'] = "" - else: - info['epoch'] = str(info['epoch']) + ":" - if not info.get('external_repo_id', 0): - buildinfo = session.getBuild(info['build_id']) - buildinfo['name'] = buildinfo['package_name'] - buildinfo['arch'] = 'src' - if buildinfo['epoch'] is None: - buildinfo['epoch'] = "" - else: - buildinfo['epoch'] = str(buildinfo['epoch']) + ":" - print("RPM: %(epoch)s%(name)s-%(version)s-%(release)s.%(arch)s [%(id)d]" % info) - if info.get('external_repo_id'): - repo = session.getExternalRepo(info['external_repo_id']) - print("External Repository: %(name)s [%(id)i]" % repo) - print("External Repository url: %(url)s" % repo) - else: - print("RPM Path: %s" % - os.path.join(koji.pathinfo.build(buildinfo), koji.pathinfo.rpm(info))) - print("SRPM: %(epoch)s%(name)s-%(version)s-%(release)s [%(id)d]" % buildinfo) - print("SRPM Path: %s" % - os.path.join(koji.pathinfo.build(buildinfo), koji.pathinfo.rpm(buildinfo))) - print("Built: %s" % time.strftime('%a, %d %b %Y %H:%M:%S %Z', - time.localtime(info['buildtime']))) - print("SIGMD5: %(payloadhash)s" % info) - print("Size: %(size)s" % info) - if not info.get('external_repo_id', 0): - headers = session.getRPMHeaders(rpmID=info['id'], - headers=["license"]) - if 'license' in headers: - print("License: %(license)s" % headers) - print("Build ID: %(build_id)s" % info) - if info['buildroot_id'] is None: - print("No buildroot data available") - else: - br_info = session.getBuildroot(info['buildroot_id']) - if br_info['br_type'] == koji.BR_TYPES['STANDARD']: - print("Buildroot: %(id)i (tag %(tag_name)s, arch %(arch)s, repo %(repo_id)i)" % - br_info) - print("Build Host: %(host_name)s" % br_info) - print("Build Task: %(task_id)i" % br_info) - else: - print("Content generator: %(cg_name)s" % br_info) - print("Buildroot: %(id)i" % br_info) - print("Build Host OS: %(host_os)s (%(host_arch)s)" % br_info) - if info.get('extra'): - print("Extra: %(extra)r" % info) - if options.buildroots: - br_list = session.listBuildroots(rpmID=info['id'], queryOpts={'order': 'buildroot.id'}) - print("Used in %i buildroots:" % len(br_list)) - if len(br_list): - print(" %8s %-28s %-8s %-29s" % ('id', 'build tag', 'arch', 'build host')) - print(" %s %s %s %s" % ('-' * 8, '-' * 28, '-' * 8, '-' * 29)) - for br_info in br_list: - print(" %(id)8i %(tag_name)-28s %(arch)-8s %(host_name)-29s" % br_info) - if error_hit: - error() - - -def anon_handle_buildinfo(goptions, session, args): - "[info] Print basic information about a build" - usage = "usage: %prog buildinfo [options] [ ...]" - parser = OptionParser(usage=get_usage_str(usage)) - parser.add_option("--changelog", action="store_true", help="Show the changelog for the build") - (options, args) = parser.parse_args(args) - if len(args) < 1: - parser.error("Please specify a build") - ensure_connection(session, goptions) - error_hit = False - for build in args: - if build.isdigit(): - build = int(build) - info = session.getBuild(build) - if info is None: - warn("No such build: %s\n" % build) - error_hit = True - continue - task = None - if info['task_id']: - task = session.getTaskInfo(info['task_id'], request=True) - taglist = [] - for tag in session.listTags(build): - taglist.append(tag['name']) - info['arch'] = 'src' - info['state'] = koji.BUILD_STATES[info['state']] - print("BUILD: %(name)s-%(version)s-%(release)s [%(id)d]" % info) - print("State: %(state)s" % info) - if info['state'] == 'BUILDING': - print("Reserved by: %(cg_name)s" % info) - print("Built by: %(owner_name)s" % info) - source = info.get('source') - if source is not None: - print("Source: %s" % source) - if 'volume_name' in info: - print("Volume: %(volume_name)s" % info) - if task: - print("Task: %s %s" % (task['id'], koji.taskLabel(task))) - else: - print("Task: none") - print("Finished: %s" % koji.formatTimeLong(info['completion_ts'])) - maven_info = session.getMavenBuild(info['id']) - if maven_info: - print("Maven groupId: %s" % maven_info['group_id']) - print("Maven artifactId: %s" % maven_info['artifact_id']) - print("Maven version: %s" % maven_info['version']) - win_info = session.getWinBuild(info['id']) - if win_info: - print("Windows build platform: %s" % win_info['platform']) - print("Tags: %s" % ' '.join(taglist)) - if info.get('extra'): - print("Extra: %(extra)r" % info) - archives_seen = {} - maven_archives = session.listArchives(buildID=info['id'], type='maven') - if maven_archives: - print("Maven archives:") - for archive in maven_archives: - archives_seen.setdefault(archive['id'], 1) - print(os.path.join(koji.pathinfo.mavenbuild(info), - koji.pathinfo.mavenfile(archive))) - win_archives = session.listArchives(buildID=info['id'], type='win') - if win_archives: - print("Windows archives:") - for archive in win_archives: - archives_seen.setdefault(archive['id'], 1) - print(os.path.join(koji.pathinfo.winbuild(info), koji.pathinfo.winfile(archive))) - img_archives = session.listArchives(buildID=info['id'], type='image') - if img_archives: - print('Image archives:') - for archive in img_archives: - archives_seen.setdefault(archive['id'], 1) - print(os.path.join(koji.pathinfo.imagebuild(info), archive['filename'])) - archive_idx = {} - for archive in session.listArchives(buildID=info['id']): - if archive['id'] in archives_seen: - continue - archive_idx.setdefault(archive['btype'], []).append(archive) - for btype in archive_idx: - archives = archive_idx[btype] - print('%s Archives:' % btype.capitalize()) - for archive in archives: - print(os.path.join(koji.pathinfo.typedir(info, btype), archive['filename'])) - rpms = session.listRPMs(buildID=info['id']) - if rpms: - with session.multicall() as mc: - for rpm in rpms: - rpm['sigs'] = mc.queryRPMSigs(rpm['id']) - print("RPMs:") - for rpm in rpms: - line = os.path.join(koji.pathinfo.build(info), koji.pathinfo.rpm(rpm)) - keys = ', '.join(sorted([x['sigkey'] for x in rpm['sigs'].result if x['sigkey']])) - if keys: - line += '\tSignatures: %s' % keys - print(line) - if options.changelog: - changelog = session.getChangelogEntries(info['id']) - if changelog: - print("Changelog:") - print(koji.util.formatChangelog(changelog)) - if error_hit: - error() - - -def anon_handle_hostinfo(goptions, session, args): - "[info] Print basic information about a host" - usage = "usage: %prog hostinfo [options] [ ...]" - parser = OptionParser(usage=get_usage_str(usage)) - (options, args) = parser.parse_args(args) - if len(args) < 1: - parser.error("Please specify a host") - ensure_connection(session, goptions) - error_hit = False - for host in args: - if host.isdigit(): - host = int(host) - info = session.getHost(host) - if info is None: - warn("No such host: %s\n" % host) - error_hit = True - continue - print("Name: %(name)s" % info) - print("ID: %(id)d" % info) - print("Arches: %(arches)s" % info) - print("Capacity: %(capacity)s" % info) - print("Task Load: %(task_load).2f" % info) - if info['description']: - description = info['description'].splitlines() - print("Description: %s" % description[0]) - for line in description[1:]: - print("%s%s" % (" " * 13, line)) - else: - print("Description:") - if info['comment']: - comment = info['comment'].splitlines() - print("Comment: %s" % comment[0]) - for line in comment[1:]: - print("%s%s" % (" " * 9, line)) - else: - print("Comment:") - print("Enabled: %s" % (info['enabled'] and 'yes' or 'no')) - print("Ready: %s" % (info['ready'] and 'yes' or 'no')) - try: - update = session.getLastHostUpdate(info['id'], ts=True) - except koji.ParameterError: - # Hubs prior to v1.25.0 do not have a "ts" parameter for getLastHostUpdate - update = session.getLastHostUpdate(info['id']) - if update is None: - update = "never" - else: - update = koji.formatTimeLong(update) - print("Last Update: %s" % update) - print("Channels: %s" % ' '.join([c['name'] - for c in session.listChannels(hostID=info['id'])])) - print("Active Buildroots:") - states = {0: "INIT", 1: "WAITING", 2: "BUILDING"} - rows = [('NAME', 'STATE', 'CREATION TIME')] - for s in range(0, 3): - for b in session.listBuildroots(hostID=info['id'], state=s): - rows.append((("%s-%s-%s" % (b['tag_name'], b['id'], b['repo_id'])), states[s], - b['create_event_time'][:b['create_event_time'].find('.')])) - if len(rows) > 1: - for row in rows: - print("%-50s %-10s %-20s" % row) - else: - print("None") - if error_hit: - error() - - -def _multicall_with_check(session, batch_size): - """Helper for running multicall inside handle_clone_tag""" - err = False - for r in session.multiCall(batch=batch_size): - if isinstance(r, dict): - warn(r['faultString']) - err = True - if err: - error('Errors during the last call. Target tag could be inconsistent.') - - -def handle_clone_tag(goptions, session, args): - "[admin] Duplicate the contents of one tag onto another tag" - usage = "usage: %prog clone-tag [options] " - usage += "\nclone-tag will create the destination tag if it does not already exist" - parser = OptionParser(usage=get_usage_str(usage)) - parser.add_option('--config', action='store_true', - help="Copy config from the source to the dest tag") - parser.add_option('--groups', action='store_true', help="Copy group information") - parser.add_option('--pkgs', action='store_true', - help="Copy package list from the source to the dest tag") - parser.add_option('--builds', action='store_true', help="Tag builds into the dest tag") - parser.add_option('--all', action='store_true', - help="The same as --config --groups --pkgs --builds") - parser.add_option('--latest-only', action='store_true', - help="Tag only the latest build of each package") - parser.add_option('--inherit-builds', action='store_true', - help="Include all builds inherited into the source tag into the dest tag") - parser.add_option('--ts', type='int', metavar="TIMESTAMP", - help='Clone tag at last event before specific timestamp') - parser.add_option('--no-delete', action='store_false', dest="delete", default=True, - help="Don't delete any existing content in dest tag.") - parser.add_option('--event', type='int', help='Clone tag at a specific event') - parser.add_option('--repo', type='int', help='Clone tag at a specific repo event') - parser.add_option("-v", "--verbose", action="store_true", help="show changes") - parser.add_option("--notify", action="store_true", default=False, - help='Send tagging/untagging notifications') - parser.add_option("-f", "--force", action="store_true", - help="override tag locks if necessary") - parser.add_option("-n", "--test", action="store_true", help="test mode") - parser.add_option("--batch", type='int', default=100, metavar='SIZE', - help="batch size of multicalls [0 to disable, default: %default]") - (options, args) = parser.parse_args(args) - - if len(args) != 2: - parser.error("This command takes two arguments: ") - activate_session(session, goptions) - - if not options.test and not (session.hasPerm('admin') or session.hasPerm('tag')): - parser.error("This action requires tag or admin privileges") - - if args[0] == args[1]: - parser.error('Source and destination tags must be different.') - - if options.batch < 0: - parser.error("batch size must be bigger than zero") - - if options.all: - options.config = options.groups = options.pkgs = options.builds = True - - event = koji.util.eventFromOpts(session, options) or {} - if event: - event['timestr'] = time.asctime(time.localtime(event['ts'])) - print("Cloning at event %(id)i (%(timestr)s)" % event) - - # store tags. - try: - srctag = session.getBuildConfig(args[0], event=event.get('id')) - except koji.GenericError: - parser.error("No such src-tag: %s" % args[0]) - dsttag = session.getTag(args[1]) - if not srctag: - parser.error("No such src-tag: %s" % args[0]) - if (srctag['locked'] and not options.force) \ - or (dsttag and dsttag['locked'] and not options.force): - parser.error("Error: You are attempting to clone from or to a tag which is locked.\n" - "Please use --force if this is what you really want to do.") - - # init debug lists. - chgpkglist = [] - chgbldlist = [] - chggrplist = [] - # case of brand new dst-tag. - if not dsttag: - # create a new tag, copy srctag header. - if not options.test: - if options.config: - session.createTag(args[1], parent=None, arches=srctag['arches'], - perm=srctag['perm_id'], - locked=srctag['locked'], - maven_support=srctag['maven_support'], - maven_include_all=srctag['maven_include_all'], - extra=srctag['extra']) - else: - session.createTag(args[1], parent=None) - # store the new tag, need its assigned id. - newtag = session.getTag(args[1], strict=True) - # get pkglist of src-tag, including inherited packages. - if options.pkgs: - srcpkgs = session.listPackages(tagID=srctag['id'], - inherited=True, - event=event.get('id')) - srcpkgs.sort(key=lambda x: x['package_name']) - if not options.test: - session.multicall = True - for pkgs in srcpkgs: - # for each package add one entry in the new tag. - chgpkglist.append(('[new]', - pkgs['package_name'], - pkgs['blocked'], - pkgs['owner_name'], - pkgs['tag_name'])) - if not options.test: - # add packages. - session.packageListAdd(newtag['name'], - pkgs['package_name'], - owner=pkgs['owner_name'], - block=pkgs['blocked'], - extra_arches=pkgs['extra_arches']) - if not options.test: - _multicall_with_check(session, options.batch) - if options.builds: - # get --all latest builds from src tag - builds = reversed(session.listTagged(srctag['id'], - event=event.get('id'), - inherit=options.inherit_builds, - latest=options.latest_only)) - if not options.test: - session.multicall = True - for build in builds: - # add missing 'name' field. - build['name'] = build['package_name'] - chgbldlist.append(('[new]', - build['package_name'], - build['nvr'], - koji.BUILD_STATES[build['state']], - build['owner_name'], - build['tag_name'])) - # copy latest builds into new tag - if not options.test: - session.tagBuildBypass(newtag['name'], - build, - force=options.force, - notify=options.notify) - if not options.test: - _multicall_with_check(session, options.batch) - if options.groups: - # Copy the group data - srcgroups = session.getTagGroups(srctag['name'], - event=event.get('id')) - if not options.test: - session.multicall = True - for group in srcgroups: - if not options.test: - session.groupListAdd(newtag['name'], group['name']) - for pkg in group['packagelist']: - if not options.test: - session.groupPackageListAdd(newtag['name'], - group['name'], - pkg['package'], - block=pkg['blocked']) - chggrplist.append(('[new]', pkg['package'], group['name'])) - if not options.test: - _multicall_with_check(session, options.batch) - # case of existing dst-tag. - if dsttag: - if options.config and not options.test: - if dsttag['extra']: - session.editTag2(dsttag['id'], remove_extra=list(dsttag['extra'].keys())) - session.editTag2(dsttag['id'], parent=None, arches=srctag['arches'], - perm=srctag['perm_id'], - locked=srctag['locked'], - maven_support=srctag['maven_support'], - maven_include_all=srctag['maven_include_all'], - extra=srctag['extra']) - dsttag = session.getTag(dsttag['id'], strict=True) - - # get fresh list of packages & builds into maps. - srcpkgs = {} - dstpkgs = {} - srcbldsbypkg = defaultdict(OrderedDict) - dstbldsbypkg = defaultdict(OrderedDict) - srcgroups = OrderedDict() - dstgroups = OrderedDict() - # we use OrderedDict so that these indexes preserve the order given to us - if options.pkgs: - for pkg in session.listPackages(tagID=srctag['id'], - inherited=True, - event=event.get('id')): - srcpkgs[pkg['package_name']] = pkg - for pkg in session.listPackages(tagID=dsttag['id'], - inherited=True): - dstpkgs[pkg['package_name']] = pkg - if options.builds: - # listTagged orders builds latest-first - # so reversing that gives us oldest-first - for build in reversed(session.listTagged(srctag['id'], - event=event.get('id'), - inherit=options.inherit_builds, - latest=options.latest_only)): - srcbldsbypkg[build['package_name']][build['nvr']] = build - # get builds in dsttag without inheritance. - # latest=False to get all builds even when latest_only = True, - # so that only the *latest* build per tag will live in. - for build in reversed(session.listTagged(dsttag['id'], - inherit=False, - latest=False)): - dstbldsbypkg[build['package_name']][build['nvr']] = build - if options.groups: - for group in session.getTagGroups(srctag['name'], - event=event.get('id')): - srcgroups[group['name']] = group - for group in session.getTagGroups(dsttag['name']): - dstgroups[group['name']] = group - # construct to-do lists. - paddlist = [] # list containing new packages to be added from src tag - for (package_name, pkg) in six.iteritems(srcpkgs): - if package_name not in dstpkgs: - paddlist.append(pkg) - paddlist.sort(key=lambda x: x['package_name']) - pdellist = [] # list containing packages no more present in dst tag - for (package_name, pkg) in six.iteritems(dstpkgs): - if package_name not in srcpkgs: - pdellist.append(pkg) - pdellist.sort(key=lambda x: x['package_name']) - baddlist = [] # list containing new builds to be added from src tag - bdellist = [] # list containing new builds to be removed from dst tag - if options.delete: - # remove builds for packages that are absent from src tag - for (pkg, dstblds) in six.iteritems(dstbldsbypkg): - if pkg not in srcbldsbypkg: - bdellist.extend(dstblds.values()) - # add and/or remove builds from dst to match src contents and order - for (pkg, srcblds) in six.iteritems(srcbldsbypkg): - dstblds = dstbldsbypkg[pkg] - ablds = [] - dblds = [] - # firstly, deal with extra builds in dst - removed_nvrs = set(dstblds.keys()) - set(srcblds.keys()) - bld_order = srcblds.copy() - if options.delete: - # mark the extra builds for deletion - dnvrs = [] - for (dstnvr, dstbld) in six.iteritems(dstblds): - if dstnvr in removed_nvrs: - dnvrs.append(dstnvr) - dblds.append(dstbld) - # we also remove them from dstblds now so that they do not - # interfere with the order comparison below - for dnvr in dnvrs: - del dstblds[dnvr] - else: - # in the no-delete case, the extra builds should be forced - # to last in the tag - bld_order = OrderedDict() - for (dstnvr, dstbld) in six.iteritems(dstblds): - if dstnvr in removed_nvrs: - bld_order[dstnvr] = dstbld - for (nvr, srcbld) in six.iteritems(srcblds): - bld_order[nvr] = srcbld - # secondly, add builds from src tag and adjust the order - for (nvr, srcbld) in six.iteritems(bld_order): - found = False - out_of_order = [] - # note that dstblds is trimmed as we go, so we are only - # considering the tail corresponding to where we are at - # in the srcblds loop - for (dstnvr, dstbld) in six.iteritems(dstblds): - if nvr == dstnvr: - found = True - break - else: - out_of_order.append(dstnvr) - dblds.append(dstbld) - for dnvr in out_of_order: - del dstblds[dnvr] - # these will be re-added in the proper order later - if found: - # remove it for next pass so we stay aligned with outer - # loop - del dstblds[nvr] - else: - # missing from dst, so we need to add it - ablds.append(srcbld) - baddlist.extend(ablds) - bdellist.extend(dblds) - baddlist.sort(key=lambda x: x['package_name']) - bdellist.sort(key=lambda x: x['package_name']) - - gaddlist = [] # list containing new groups to be added from src tag - for (grpname, group) in six.iteritems(srcgroups): - if grpname not in dstgroups: - gaddlist.append(group) - gdellist = [] # list containing groups to be removed from src tag - for (grpname, group) in six.iteritems(dstgroups): - if grpname not in srcgroups: - gdellist.append(group) - grpchanges = OrderedDict() # dict of changes to make in shared groups - for (grpname, group) in six.iteritems(srcgroups): - if grpname in dstgroups: - dstgroup = dstgroups[grpname] - grpchanges[grpname] = {'adds': [], 'dels': []} - # Store whether group is inherited or not - grpchanges[grpname]['inherited'] = False - if dstgroup['tag_id'] != dsttag['id']: - grpchanges[grpname]['inherited'] = True - srcgrppkglist = [] - dstgrppkglist = [] - for pkg in group['packagelist']: - srcgrppkglist.append(pkg['package']) - for pkg in dstgroups[grpname]['packagelist']: - dstgrppkglist.append(pkg['package']) - for pkg in srcgrppkglist: - if pkg not in dstgrppkglist: - grpchanges[grpname]['adds'].append(pkg) - for pkg in dstgrppkglist: - if pkg not in srcgrppkglist: - grpchanges[grpname]['dels'].append(pkg) - # ADD new packages. - if not options.test: - session.multicall = True - for pkg in paddlist: - chgpkglist.append(('[add]', - pkg['package_name'], - pkg['blocked'], - pkg['owner_name'], - pkg['tag_name'])) - if not options.test: - session.packageListAdd(dsttag['name'], - pkg['package_name'], - owner=pkg['owner_name'], - block=pkg['blocked'], - extra_arches=pkg['extra_arches']) - if not options.test: - _multicall_with_check(session, options.batch) - # DEL builds. To keep the order we should untag builds at first - if not options.test: - session.multicall = True - for build in bdellist: - # don't delete an inherited build. - if build['tag_name'] == dsttag['name']: - # add missing 'name' field - build['name'] = build['package_name'] - chgbldlist.append(('[del]', - build['package_name'], - build['nvr'], - koji.BUILD_STATES[build['state']], - build['owner_name'], - build['tag_name'])) - # go on del builds from new tag. - if not options.test: - session.untagBuildBypass(dsttag['name'], - build, - force=options.force, - notify=options.notify) - if not options.test: - _multicall_with_check(session, options.batch) - # ADD builds. - if not options.test: - session.multicall = True - for build in baddlist: - # add missing 'name' field. - build['name'] = build['package_name'] - chgbldlist.append(('[add]', - build['package_name'], - build['nvr'], - koji.BUILD_STATES[build['state']], - build['owner_name'], - build['tag_name'])) - # copy latest builds into new tag. - if not options.test: - session.tagBuildBypass(dsttag['name'], - build, - force=options.force, - notify=options.notify) - if not options.test: - _multicall_with_check(session, options.batch) - # ADD groups. - if not options.test: - session.multicall = True - for group in gaddlist: - if not options.test: - session.groupListAdd(dsttag['name'], - group['name'], - force=options.force) - for pkg in group['packagelist']: - if not options.test: - session.groupPackageListAdd(dsttag['name'], - group['name'], - pkg['package'], - force=options.force) - chggrplist.append(('[new]', pkg['package'], group['name'])) - if not options.test: - _multicall_with_check(session, options.batch) - # ADD group pkgs. - if not options.test: - session.multicall = True - for group in grpchanges: - for pkg in grpchanges[group]['adds']: - chggrplist.append(('[new]', pkg, group)) - if not options.test: - session.groupPackageListAdd(dsttag['name'], - group, - pkg, - force=options.force) - if not options.test: - _multicall_with_check(session, options.batch) - if options.delete: - # DEL packages - ninhrtpdellist = [] - inhrtpdellist = [] - for pkg in pdellist: - if pkg['tag_name'] == dsttag['name']: - ninhrtpdellist.append(pkg) - else: - inhrtpdellist.append(pkg) - session.multicall = True - # delete only non-inherited packages. - for pkg in ninhrtpdellist: - # check if package have owned builds inside. - session.listTagged(dsttag['name'], - package=pkg['package_name'], - inherit=False) - bump_builds = session.multiCall(batch=options.batch) - if not options.test: - session.multicall = True - for pkg, [builds] in zip(ninhrtpdellist, bump_builds): - if isinstance(builds, dict): - error(builds['faultString']) - # remove all its builds first if there are any. - for build in builds: - # add missing 'name' field. - build['name'] = build['package_name'] - chgbldlist.append(('[del]', - build['package_name'], - build['nvr'], - koji.BUILD_STATES[build['state']], - build['owner_name'], - build['tag_name'])) - # so delete latest build(s) from new tag. - if not options.test: - session.untagBuildBypass(dsttag['name'], - build, - force=options.force, - notify=options.notify) - # now safe to remove package itself since we resolved its builds. - chgpkglist.append(('[del]', - pkg['package_name'], - pkg['blocked'], - pkg['owner_name'], - pkg['tag_name'])) - if not options.test: - session.packageListRemove(dsttag['name'], - pkg['package_name'], - force=False) - # mark as blocked inherited packages. - for pkg in inhrtpdellist: - chgpkglist.append(('[blk]', - pkg['package_name'], - pkg['blocked'], - pkg['owner_name'], - pkg['tag_name'])) - if not options.test: - session.packageListBlock(dsttag['name'], pkg['package_name']) - if not options.test: - _multicall_with_check(session, options.batch) - # DEL groups. - if not options.test: - session.multicall = True - for group in gdellist: - # Only delete a group that isn't inherited - if group['tag_id'] == dsttag['id']: - if not options.test: - session.groupListRemove(dsttag['name'], - group['name'], - force=options.force) - for pkg in group['packagelist']: - chggrplist.append(('[del]', pkg['package'], group['name'])) - # mark as blocked inherited groups. - else: - if not options.test: - session.groupListBlock(dsttag['name'], group['name']) - for pkg in group['packagelist']: - chggrplist.append(('[blk]', pkg['package'], group['name'])) - if not options.test: - _multicall_with_check(session, options.batch) - # DEL group pkgs. - if not options.test: - session.multicall = True - for group in grpchanges: - for pkg in grpchanges[group]['dels']: - # Only delete a group that isn't inherited - if not grpchanges[group]['inherited']: - chggrplist.append(('[del]', pkg, group)) - if not options.test: - session.groupPackageListRemove(dsttag['name'], - group, - pkg, - force=options.force) - else: - chggrplist.append(('[blk]', pkg, group)) - if not options.test: - session.groupPackageListBlock(dsttag['name'], - group, - pkg) - if not options.test: - _multicall_with_check(session, options.batch) - # print final list of actions. - if options.verbose: - pfmt = ' %-7s %-28s %-10s %-10s %-10s\n' - bfmt = ' %-7s %-28s %-40s %-10s %-10s %-10s\n' - gfmt = ' %-7s %-28s %-28s\n' - sys.stdout.write('\nList of changes:\n\n') - sys.stdout.write(pfmt % ('Action', 'Package', 'Blocked', 'Owner', 'From Tag')) - sys.stdout.write(pfmt % ('-' * 7, '-' * 28, '-' * 10, '-' * 10, '-' * 10)) - for changes in chgpkglist: - sys.stdout.write(pfmt % changes) - sys.stdout.write('\n') - sys.stdout.write(bfmt % - ('Action', 'From/To Package', 'Build(s)', 'State', 'Owner', 'From Tag')) - sys.stdout.write(bfmt % ('-' * 7, '-' * 28, '-' * 40, '-' * 10, '-' * 10, '-' * 10)) - for changes in chgbldlist: - sys.stdout.write(bfmt % changes) - sys.stdout.write('\n') - sys.stdout.write(gfmt % ('Action', 'Package', 'Group')) - sys.stdout.write(gfmt % ('-' * 7, '-' * 28, '-' * 28)) - for changes in chggrplist: - sys.stdout.write(gfmt % changes) - - -def handle_add_target(goptions, session, args): - "[admin] Create a new build target" - usage = "usage: %prog add-target " - parser = OptionParser(usage=get_usage_str(usage)) - (options, args) = parser.parse_args(args) - if len(args) < 2: - parser.error("Please specify a target name, a build tag, and destination tag") - elif len(args) > 3: - parser.error("Incorrect number of arguments") - name = args[0] - build_tag = args[1] - if len(args) > 2: - dest_tag = args[2] - else: - # most targets have the same name as their destination - dest_tag = name - activate_session(session, goptions) - if not (session.hasPerm('admin') or session.hasPerm('target')): - parser.error("This action requires target or admin privileges") - - chkbuildtag = session.getTag(build_tag) - chkdesttag = session.getTag(dest_tag) - if not chkbuildtag: - error("No such tag: %s" % build_tag) - if not chkbuildtag.get("arches", None): - error("Build tag has no arches: %s" % build_tag) - if not chkdesttag: - error("No such destination tag: %s" % dest_tag) - - session.createBuildTarget(name, build_tag, dest_tag) - - -def handle_edit_target(goptions, session, args): - "[admin] Set the name, build_tag, and/or dest_tag of an existing build target to new values" - usage = "usage: %prog edit-target [options] " - parser = OptionParser(usage=get_usage_str(usage)) - parser.add_option("--rename", help="Specify new name for target") - parser.add_option("--build-tag", help="Specify a different build tag") - parser.add_option("--dest-tag", help="Specify a different destination tag") - - (options, args) = parser.parse_args(args) - - if len(args) != 1: - parser.error("Please specify a build target") - activate_session(session, goptions) - - if not (session.hasPerm('admin') or session.hasPerm('target')): - parser.error("This action requires target or admin privileges") - - targetInfo = session.getBuildTarget(args[0]) - if targetInfo is None: - raise koji.GenericError("No such build target: %s" % args[0]) - - targetInfo['orig_name'] = targetInfo['name'] - - if options.rename: - targetInfo['name'] = options.rename - if options.build_tag: - targetInfo['build_tag_name'] = options.build_tag - chkbuildtag = session.getTag(options.build_tag) - if not chkbuildtag: - error("No such tag: %s" % options.build_tag) - if not chkbuildtag.get("arches", None): - error("Build tag has no arches: %s" % options.build_tag) - if options.dest_tag: - chkdesttag = session.getTag(options.dest_tag) - if not chkdesttag: - error("No such destination tag: %s" % options.dest_tag) - targetInfo['dest_tag_name'] = options.dest_tag - - session.editBuildTarget(targetInfo['orig_name'], targetInfo['name'], - targetInfo['build_tag_name'], targetInfo['dest_tag_name']) - - -def handle_remove_target(goptions, session, args): - "[admin] Remove a build target" - usage = "usage: %prog remove-target [options] " - parser = OptionParser(usage=get_usage_str(usage)) - (options, args) = parser.parse_args(args) - - if len(args) != 1: - parser.error("Please specify a build target to remove") - activate_session(session, goptions) - - if not (session.hasPerm('admin') or session.hasPerm('target')): - parser.error("This action requires target or admin privileges") - - target = args[0] - target_info = session.getBuildTarget(target) - if not target_info: - error("No such build target: %s" % target) - - session.deleteBuildTarget(target_info['id']) - - -def handle_remove_tag(goptions, session, args): - "[admin] Remove a tag" - usage = "usage: %prog remove-tag [options] " - parser = OptionParser(usage=get_usage_str(usage)) - (options, args) = parser.parse_args(args) - - if len(args) != 1: - parser.error("Please specify a tag to remove") - activate_session(session, goptions) - - if not (session.hasPerm('admin') or session.hasPerm('tag')): - parser.error("This action requires tag or admin privileges") - - tag = args[0] - tag_info = session.getTag(tag) - if not tag_info: - error("No such tag: %s" % tag) - - session.deleteTag(tag_info['id']) - - -def anon_handle_list_targets(goptions, session, args): - "[info] List the build targets" - usage = "usage: %prog list-targets [options]" - parser = OptionParser(usage=get_usage_str(usage)) - parser.add_option("--name", help="Specify the build target name") - parser.add_option("--quiet", action="store_true", default=goptions.quiet, - help="Do not print the header information") - (options, args) = parser.parse_args(args) - - if len(args) != 0: - parser.error("This command takes no arguments") - ensure_connection(session, goptions) - - targets = session.getBuildTargets(options.name) - if len(targets) == 0: - if options.name: - parser.error('No such build target: %s' % options.name) - else: - parser.error('No targets were found') - - fmt = "%(name)-30s %(build_tag_name)-30s %(dest_tag_name)-30s" - if not options.quiet: - print("%-30s %-30s %-30s" % ('Name', 'Buildroot', 'Destination')) - print("-" * 93) - tmp_list = sorted([(x['name'], x) for x in targets]) - targets = [x[1] for x in tmp_list] - for target in targets: - print(fmt % target) - # pprint.pprint(session.getBuildTargets()) - - -def _printInheritance(tags, sibdepths=None, reverse=False): - if len(tags) == 0: - return - if sibdepths is None: - sibdepths = [] - currtag = tags[0] - tags = tags[1:] - if reverse: - siblings = len([tag for tag in tags if tag['parent_id'] == currtag['parent_id']]) - else: - siblings = len([tag for tag in tags if tag['child_id'] == currtag['child_id']]) - - sys.stdout.write(format_inheritance_flags(currtag)) - outdepth = 0 - for depth in sibdepths: - if depth < currtag['currdepth']: - outspacing = depth - outdepth - sys.stdout.write(' ' * (outspacing * 3 - 1)) - sys.stdout.write(_printable_unicode(u'\u2502')) - outdepth = depth - - sys.stdout.write(' ' * ((currtag['currdepth'] - outdepth) * 3 - 1)) - if siblings: - sys.stdout.write(_printable_unicode(u'\u251c')) - else: - sys.stdout.write(_printable_unicode(u'\u2514')) - sys.stdout.write(_printable_unicode(u'\u2500')) - if reverse: - sys.stdout.write('%(name)s (%(tag_id)i)\n' % currtag) - else: - sys.stdout.write('%(name)s (%(parent_id)i)\n' % currtag) - - if siblings: - if len(sibdepths) == 0 or sibdepths[-1] != currtag['currdepth']: - sibdepths.append(currtag['currdepth']) - else: - if len(sibdepths) > 0 and sibdepths[-1] == currtag['currdepth']: - sibdepths.pop() - - _printInheritance(tags, sibdepths, reverse) - - -def anon_handle_list_tag_inheritance(goptions, session, args): - "[info] Print the inheritance information for a tag" - usage = """\ - usage: %prog list-tag-inheritance [options] - - Prints tag inheritance with basic information about links. - Four flags could be seen in the output: - M - maxdepth - limits inheritance to n-levels - F - package filter (packages ignored for inheritance) - I - intransitive link - inheritance immediately stops here - N - noconfig - if tag is used in buildroot, its configuration values will not be used - - Exact values for maxdepth and package filter can be inquired by taginfo command. - """ - usage = textwrap.dedent(usage) - parser = OptionParser(usage=get_usage_str(usage)) - parser.add_option("--reverse", action="store_true", - help="Process tag's children instead of its parents") - parser.add_option("--stop", help=SUPPRESS_HELP) - parser.add_option("--jump", help=SUPPRESS_HELP) - parser.add_option("--event", type='int', metavar="EVENT#", help="query at event") - parser.add_option("--ts", type='int', metavar="TIMESTAMP", - help="query at last event before timestamp") - parser.add_option("--repo", type='int', metavar="REPO#", help="query at event for a repo") - (options, args) = parser.parse_args(args) - if len(args) != 1: - parser.error("This command takes exactly one argument: a tag name or ID") - for deprecated in ('stop', 'jump'): - if getattr(options, deprecated): - parser.error("--%s option has been removed in 1.26" % deprecated) - ensure_connection(session, goptions) - event = koji.util.eventFromOpts(session, options) - if event: - event['timestr'] = time.asctime(time.localtime(event['ts'])) - print("Querying at event %(id)i (%(timestr)s)" % event) - if event: - tag = session.getTag(args[0], event=event['id']) - else: - tag = session.getTag(args[0]) - if not tag: - parser.error("No such tag: %s" % args[0]) - - opts = {} - opts['reverse'] = options.reverse or False - if event: - opts['event'] = event['id'] - - sys.stdout.write(' %s (%i)\n' % (tag['name'], tag['id'])) - data = session.getFullInheritance(tag['id'], **opts) - _printInheritance(data, None, opts['reverse']) - - -def anon_handle_list_tags(goptions, session, args): - "[info] Print the list of tags" - usage = "usage: %prog list-tags [options] [pattern]" - parser = OptionParser(usage=get_usage_str(usage)) - parser.add_option("--show-id", action="store_true", help="Show tag ids") - parser.add_option("--verbose", action="store_true", help="Show more information") - parser.add_option("--unlocked", action="store_true", help="Only show unlocked tags") - parser.add_option("--build", help="Show tags associated with a build") - parser.add_option("--package", help="Show tags associated with a package") - (options, patterns) = parser.parse_args(args) - ensure_connection(session, goptions) - - pkginfo = {} - buildinfo = {} - - if options.package: - pkginfo = session.getPackage(options.package) - if not pkginfo: - parser.error("No such package: %s" % options.package) - - if options.build: - buildinfo = session.getBuild(options.build) - if not buildinfo: - parser.error("No such build: %s" % options.build) - - if not patterns: - # list everything if no pattern is supplied - tags = session.listTags(build=buildinfo.get('id', None), - package=pkginfo.get('id', None)) - else: - # The hub may not support the pattern option. We try with that first - # and fall back to the old way. - fallback = False - try: - tags = [] - with session.multicall(strict=True) as m: - for pattern in patterns: - tags.append(m.listTags(build=buildinfo.get('id', None), - package=pkginfo.get('id', None), - pattern=pattern)) - tags = list(itertools.chain(*[t.result for t in tags])) - except koji.ParameterError: - fallback = True - if fallback: - # without the pattern option, we have to filter client side - tags = session.listTags(buildinfo.get('id', None), pkginfo.get('id', None)) - tags = [t for t in tags if koji.util.multi_fnmatch(t['name'], patterns)] - - tags.sort(key=lambda x: x['name']) - # if options.verbose: - # fmt = "%(name)s [%(id)i] %(perm)s %(locked)s %(arches)s" - if options.show_id: - fmt = "%(name)s [%(id)i]" - else: - fmt = "%(name)s" - for tag in tags: - if options.unlocked: - if tag['locked'] or tag['perm']: - continue - if not options.verbose: - print(fmt % tag) - else: - sys.stdout.write(fmt % tag) - if tag['locked']: - sys.stdout.write(' [LOCKED]') - if tag['perm']: - sys.stdout.write(' [%(perm)s perm required]' % tag) - print('') - - -def _print_histline(entry, **kwargs): - options = kwargs['options'] - event_id, table, create, x = entry - who = None - edit = x.get('.related') - if edit: - del x['.related'] - bad_edit = None - if len(edit) != 1: - bad_edit = "%i elements" % (len(edit) + 1) - other = edit[0] - # check edit for sanity - if create or not other[2]: - bad_edit = "out of order" - if event_id != other[0]: - bad_edit = "non-matching" - if bad_edit: - warn("Unusual edit at event %i in table %s (%s)" % (event_id, table, bad_edit)) - # we'll simply treat them as separate events - pprint.pprint(entry) - pprint.pprint(edit) - _print_histline(entry, **kwargs) - for data in edit: - _print_histline(entry, **kwargs) - return - if create: - ts = x['create_ts'] - if 'creator_name' in x: - who = "by %(creator_name)s" - else: - ts = x['revoke_ts'] - if 'revoker_name' in x: - who = "by %(revoker_name)s" - if table == 'tag_listing': - if edit: - fmt = "%(name)s-%(version)s-%(release)s re-tagged into %(tag.name)s" - elif create: - fmt = "%(name)s-%(version)s-%(release)s tagged into %(tag.name)s" - else: - fmt = "%(name)s-%(version)s-%(release)s untagged from %(tag.name)s" - elif table == 'user_perms': - if edit: - fmt = "permission %(permission.name)s re-granted to %(user.name)s" - elif create: - fmt = "permission %(permission.name)s granted to %(user.name)s" - else: - fmt = "permission %(permission.name)s revoked for %(user.name)s" - elif table == 'user_groups': - if edit: - fmt = "user %(user.name)s re-added to group %(group.name)s" - elif create: - fmt = "user %(user.name)s added to group %(group.name)s" - else: - fmt = "user %(user.name)s removed from group %(group.name)s" - elif table == 'cg_users': - if edit: - fmt = "user %(user.name)s re-added to content generator %(content_generator.name)s" - elif create: - fmt = "user %(user.name)s added to content generator %(content_generator.name)s" - else: - fmt = "user %(user.name)s removed from content generator %(content_generator.name)s" - elif table == 'tag_packages': - if edit: - fmt = "package list entry for %(package.name)s in %(tag.name)s updated" - elif create: - fmt = "package list entry created: %(package.name)s in %(tag.name)s" - else: - fmt = "package list entry revoked: %(package.name)s in %(tag.name)s" - elif table == 'tag_package_owners': - if edit: - fmt = "package owner changed for %(package.name)s in %(tag.name)s" - elif create: - fmt = "package owner %(owner.name)s set for %(package.name)s in %(tag.name)s" - else: - fmt = "package owner %(owner.name)s revoked for %(package.name)s in %(tag.name)s" - elif table == 'tag_inheritance': - if edit: - fmt = "inheritance line %(tag.name)s->%(parent.name)s updated" - elif create: - fmt = "inheritance line %(tag.name)s->%(parent.name)s added" - else: - fmt = "inheritance line %(tag.name)s->%(parent.name)s removed" - elif table == 'tag_config': - if edit: - fmt = "tag configuration for %(tag.name)s altered" - elif create: - fmt = "new tag: %(tag.name)s" - else: - fmt = "tag deleted: %(tag.name)s" - elif table == 'tag_extra': - if edit: - fmt = "tag option %(key)s for tag %(tag.name)s altered" - elif create: - fmt = "added tag option %(key)s for tag %(tag.name)s" - else: - fmt = "tag option %(key)s removed for %(tag.name)s" - elif table == 'host_config': - if edit: - fmt = "host configuration for %(host.name)s altered" - elif create: - fmt = "new host: %(host.name)s" - else: - fmt = "host deleted: %(host.name)s" - elif table == 'host_channels': - if create: - fmt = "host %(host.name)s added to channel %(channels.name)s" - else: - fmt = "host %(host.name)s removed from channel %(channels.name)s" - elif table == 'build_target_config': - if edit: - fmt = "build target configuration for %(build_target.name)s updated" - elif create: - fmt = "new build target: %(build_target.name)s" - else: - fmt = "build target deleted: %(build_target.name)s" - elif table == 'external_repo_config': - if edit: - fmt = "external repo configuration for %(external_repo.name)s altered" - elif create: - fmt = "new external repo: %(external_repo.name)s" - else: - fmt = "external repo deleted: %(external_repo.name)s" - elif table == 'tag_external_repos': - if edit: - fmt = "external repo entry for %(external_repo.name)s in tag %(tag.name)s updated" - elif create: - fmt = "external repo entry for %(external_repo.name)s added to tag %(tag.name)s" - else: - fmt = "external repo entry for %(external_repo.name)s removed from tag %(tag.name)s" - elif table == 'group_config': - if edit: - fmt = "group %(group.name)s configuration for tag %(tag.name)s updated" - elif create: - fmt = "group %(group.name)s added to tag %(tag.name)s" - else: - fmt = "group %(group.name)s removed from tag %(tag.name)s" - elif table == 'group_req_listing': - if edit: - fmt = "group dependency %(group.name)s->%(req.name)s updated in tag %(tag.name)s" - elif create: - fmt = "group dependency %(group.name)s->%(req.name)s added in tag %(tag.name)s" - else: - fmt = "group dependency %(group.name)s->%(req.name)s dropped from tag %(tag.name)s" - elif table == 'group_package_listing': - if edit: - fmt = "package entry %(package)s in group %(group.name)s, tag %(tag.name)s updated" - elif create: - fmt = "package %(package)s added to group %(group.name)s in tag %(tag.name)s" - else: - fmt = "package %(package)s removed from group %(group.name)s in tag %(tag.name)s" - else: - if edit: - fmt = "%s entry updated" % table - elif create: - fmt = "%s entry created" % table - else: - fmt = "%s entry revoked" % table - if options.utc: - time_str = time.asctime(datetime.fromtimestamp(ts, tzutc()).timetuple()) - else: - time_str = time.asctime(time.localtime(ts)) - - parts = [time_str, fmt % x] - if options.events or options.verbose: - parts.insert(1, "(eid %i)" % event_id) - if who: - parts.append(who % x) - if create and x['active']: - parts.append("[still active]") - print(' '.join(parts)) - hidden_fields = ['active', 'create_event', 'revoke_event', 'creator_id', 'revoker_id', - 'creator_name', 'revoker_name', 'create_ts', 'revoke_ts'] - - def get_nkey(key): - if key == 'perm_id': - return 'permission.name' - elif key.endswith('_id'): - return '%s.name' % key[:-3] - else: - return '%s.name' % key - if edit: - keys = sorted(to_list(x.keys())) - y = other[-1] - for key in keys: - if key in hidden_fields: - continue - if x[key] == y[key]: - continue - if key[0] == '_': - continue - nkey = get_nkey(key) - if nkey in x and nkey in y: - continue - print(" %s: %s -> %s" % (key, x[key], y[key])) - elif create and options.verbose and table != 'tag_listing': - keys = sorted(to_list(x.keys())) - # the table keys have already been represented in the base format string - also_hidden = list(_table_keys[table]) - also_hidden.extend([get_nkey(k) for k in also_hidden]) - for key in keys: - if key in hidden_fields or key in also_hidden: - continue - nkey = get_nkey(key) - if nkey in x: - continue - if key[0] == '_': - continue - if x.get('blocked') and key != 'blocked': - continue - if key.endswith('.name'): - dkey = key[:-5] - else: - dkey = key - print(" %s: %s" % (dkey, x[key])) - - -_table_keys = { - 'user_perms': ['user_id', 'perm_id'], - 'user_groups': ['user_id', 'group_id'], - 'cg_users': ['user_id', 'cg_id'], - 'tag_inheritance': ['tag_id', 'parent_id'], - 'tag_config': ['tag_id'], - 'tag_extra': ['tag_id', 'key'], - 'build_target_config': ['build_target_id'], - 'external_repo_config': ['external_repo_id'], - 'host_config': ['host_id'], - 'host_channels': ['host_id', 'channel_id'], - 'tag_external_repos': ['tag_id', 'external_repo_id'], - 'tag_listing': ['build_id', 'tag_id'], - 'tag_packages': ['package_id', 'tag_id'], - 'tag_package_owners': ['package_id', 'tag_id'], - 'group_config': ['group_id', 'tag_id'], - 'group_req_listing': ['group_id', 'tag_id', 'req_id'], - 'group_package_listing': ['group_id', 'tag_id', 'package'], -} - - -def anon_handle_list_history(goptions, session, args): - "[info] Display historical data" - usage = "usage: %prog list-history [options]" - parser = OptionParser(usage=get_usage_str(usage), option_class=TimeOption) - # Don't use local debug option, this one stays here for backward compatibility - # https://pagure.io/koji/issue/2084 - parser.add_option("--debug", action="store_true", default=goptions.debug, help=SUPPRESS_HELP) - parser.add_option("--build", help="Only show data for a specific build") - parser.add_option("--package", help="Only show data for a specific package") - parser.add_option("--tag", help="Only show data for a specific tag") - parser.add_option("--editor", "--by", metavar="USER", - help="Only show entries modified by user") - parser.add_option("--user", help="Only show entries affecting a user") - parser.add_option("--permission", help="Only show entries relating to a given permission") - parser.add_option("--cg", help="Only show entries relating to a given content generator") - parser.add_option("--external-repo", "--erepo", - help="Only show entries relating to a given external repo") - parser.add_option("--build-target", "--target", - help="Only show entries relating to a given build target") - parser.add_option("--group", help="Only show entries relating to a given group") - parser.add_option("--host", help="Only show entries related to given host") - parser.add_option("--channel", help="Only show entries related to given channel") - parser.add_option("--xkey", help="Only show entries related to given tag extra key") - parser.add_option("--before", type="time", - help="Only show entries before this time, " + TimeOption.get_help()) - parser.add_option("--after", type="time", - help="Only show entries after timestamp (same format as for --before)") - parser.add_option("--before-event", metavar="EVENT_ID", type='int', - help="Only show entries before event") - parser.add_option("--after-event", metavar="EVENT_ID", type='int', - help="Only show entries after event") - parser.add_option("--watch", action="store_true", help="Monitor history data") - parser.add_option("--active", action='store_true', - help="Only show entries that are currently active") - parser.add_option("--revoked", action='store_false', dest='active', - help="Only show entries that are currently revoked") - parser.add_option("--context", action="store_true", help="Show related entries") - parser.add_option("-s", "--show", action="append", help="Show data from selected tables") - parser.add_option("-v", "--verbose", action="store_true", help="Show more detail") - parser.add_option("-e", "--events", action="store_true", help="Show event ids") - parser.add_option("--all", action="store_true", - help="Allows listing the entire global history") - parser.add_option("--utc", action="store_true", - help="Shows time in UTC timezone") - (options, args) = parser.parse_args(args) - if len(args) != 0: - parser.error("This command takes no arguments") - kwargs = {} - limited = False - for opt in ('package', 'tag', 'build', 'editor', 'user', 'permission', - 'cg', 'external_repo', 'build_target', 'group', 'before', - 'after', 'host', 'channel', 'xkey'): - val = getattr(options, opt) - if val: - kwargs[opt] = val - limited = True - if options.before_event: - kwargs['beforeEvent'] = options.before_event - if options.after_event: - kwargs['afterEvent'] = options.after_event - if options.active is not None: - kwargs['active'] = options.active - if options.host: - hostinfo = session.getHost(options.host) - if not hostinfo: - error("No such host: %s" % options.host) - if options.channel: - channelinfo = session.getChannel(options.channel) - if not channelinfo: - error("No such channel: %s" % options.channel) - if options.utc: - kwargs['utc'] = options.utc - tables = None - if options.show: - tables = [] - for arg in options.show: - tables.extend(arg.split(',')) - if not limited and not options.all: - parser.error("Please specify an option to limit the query") - - ensure_connection(session, goptions) - - if options.watch: - if not kwargs.get('afterEvent') and not kwargs.get('after'): - kwargs['afterEvent'] = session.getLastEvent()['id'] - - while True: - histdata = session.queryHistory(tables=tables, **kwargs) - timeline = [] - - def distinguish_match(x, name): - """determine if create or revoke event matched""" - if options.context: - return True - name = "_" + name - ret = True - for key in x: - if key.startswith(name): - ret = ret and x[key] - return ret - for table in histdata: - hist = histdata[table] - for x in hist: - if x['revoke_event'] is not None: - if distinguish_match(x, 'revoked'): - timeline.append((x['revoke_event'], table, 0, x.copy())) - # pprint.pprint(timeline[-1]) - if distinguish_match(x, 'created'): - timeline.append((x['create_event'], table, 1, x)) - timeline.sort(key=lambda entry: entry[:3]) - # group edits together - new_timeline = [] - last_event = None - edit_index = {} - for entry in timeline: - event_id, table, create, x = entry - if event_id != last_event: - edit_index = {} - last_event = event_id - key = tuple([x[k] for k in _table_keys[table]]) - prev = edit_index.get((table, event_id), {}).get(key) - if prev: - prev[-1].setdefault('.related', []).append(entry) - else: - edit_index.setdefault((table, event_id), {})[key] = entry - new_timeline.append(entry) - for entry in new_timeline: - if options.debug: - print("%r" % list(entry)) - _print_histline(entry, options=options) - if not options.watch: - break - else: - time.sleep(goptions.poll_interval) - # repeat query for later events - if last_event: - kwargs['afterEvent'] = last_event - - -def _handleMap(lines, data, prefix=''): - for key, val in data.items(): - if key != '__starstar': - lines.append(' %s%s: %s' % (prefix, key, val)) - - -def _handleOpts(lines, opts, prefix=''): - if opts: - lines.append("%sOptions:" % prefix) - _handleMap(lines, opts, prefix) - - -def _parseTaskParams(session, method, task_id, topdir): - try: - return _do_parseTaskParams(session, method, task_id, topdir) - except Exception: - logger = logging.getLogger("koji") - if logger.isEnabledFor(logging.DEBUG): - tb_str = ''.join(traceback.format_exception(*sys.exc_info())) - logger.debug(tb_str) - return ['Unable to parse task parameters'] - - -def _do_parseTaskParams(session, method, task_id, topdir): - """Parse the return of getTaskRequest()""" - params = session.getTaskRequest(task_id) - - lines = [] - - if method == 'buildSRPMFromCVS': - lines.append("CVS URL: %s" % params[0]) - elif method == 'buildSRPMFromSCM': - lines.append("SCM URL: %s" % params[0]) - elif method == 'buildArch': - lines.append("SRPM: %s/work/%s" % (topdir, params[0])) - lines.append("Build Tag: %s" % session.getTag(params[1])['name']) - lines.append("Build Arch: %s" % params[2]) - lines.append("SRPM Kept: %r" % params[3]) - if len(params) > 4: - _handleOpts(lines, params[4]) - elif method == 'tagBuild': - build = session.getBuild(params[1]) - lines.append("Destination Tag: %s" % session.getTag(params[0])['name']) - lines.append("Build: %s" % koji.buildLabel(build)) - elif method == 'buildNotification': - build = params[1] - buildTarget = params[2] - lines.append("Recipients: %s" % (", ".join(params[0]))) - lines.append("Build: %s" % koji.buildLabel(build)) - lines.append("Build Target: %s" % buildTarget['name']) - lines.append("Web URL: %s" % params[3]) - elif method == 'build': - lines.append("Source: %s" % params[0]) - lines.append("Build Target: %s" % params[1]) - if len(params) > 2: - _handleOpts(lines, params[2]) - elif method == 'maven': - lines.append("SCM URL: %s" % params[0]) - lines.append("Build Target: %s" % params[1]) - if len(params) > 2: - _handleOpts(lines, params[2]) - elif method == 'buildMaven': - lines.append("SCM URL: %s" % params[0]) - lines.append("Build Tag: %s" % params[1]['name']) - if len(params) > 2: - _handleOpts(lines, params[2]) - elif method == 'wrapperRPM': - lines.append("Spec File URL: %s" % params[0]) - lines.append("Build Tag: %s" % params[1]['name']) - if params[2]: - lines.append("Build: %s" % koji.buildLabel(params[2])) - if params[3]: - lines.append("Task: %s %s" % (params[3]['id'], koji.taskLabel(params[3]))) - if len(params) > 4: - _handleOpts(lines, params[4]) - elif method == 'chainmaven': - lines.append("Builds:") - for package, opts in params[0].items(): - lines.append(" " + package) - _handleMap(lines, opts, prefix=" ") - lines.append("Build Target: %s" % params[1]) - if len(params) > 2: - _handleOpts(lines, params[2]) - elif method == 'winbuild': - lines.append("VM: %s" % params[0]) - lines.append("SCM URL: %s" % params[1]) - lines.append("Build Target: %s" % params[2]) - if len(params) > 3: - _handleOpts(lines, params[3]) - elif method == 'vmExec': - lines.append("VM: %s" % params[0]) - lines.append("Exec Params:") - for info in params[1]: - if isinstance(info, dict): - _handleMap(lines, info, prefix=' ') - else: - lines.append(" %s" % info) - if len(params) > 2: - _handleOpts(lines, params[2]) - elif method in ('createLiveCD', 'createAppliance', 'createLiveMedia'): - argnames = ['Name', 'Version', 'Release', 'Arch', 'Target Info', 'Build Tag', 'Repo', - 'Kickstart File'] - for n, v in zip(argnames, params): - lines.append("%s: %s" % (n, v)) - if len(params) > 8: - _handleOpts(lines, params[8]) - elif method in ('appliance', 'livecd', 'livemedia'): - argnames = ['Name', 'Version', 'Arches', 'Target', 'Kickstart'] - for n, v in zip(argnames, params): - lines.append("%s: %s" % (n, v)) - if len(params) > 5: - _handleOpts(lines, params[5]) - elif method == 'newRepo': - tag = session.getTag(params[0]) - lines.append("Tag: %s" % tag['name']) - elif method == 'prepRepo': - lines.append("Tag: %s" % params[0]['name']) - elif method == 'createrepo': - lines.append("Repo ID: %i" % params[0]) - lines.append("Arch: %s" % params[1]) - oldrepo = params[2] - if oldrepo: - lines.append("Old Repo ID: %i" % oldrepo['id']) - lines.append("Old Repo Creation: %s" % koji.formatTimeLong(oldrepo['create_ts'])) - if len(params) > 3: - lines.append("External Repos: %s" % - ', '.join([ext['external_repo_name'] for ext in params[3]])) - elif method == 'tagNotification': - destTag = session.getTag(params[2]) - srcTag = None - if params[3]: - srcTag = session.getTag(params[3]) - build = session.getBuild(params[4]) - user = session.getUser(params[5]) - - lines.append("Recipients: %s" % ", ".join(params[0])) - lines.append("Successful?: %s" % (params[1] and 'yes' or 'no')) - lines.append("Tagged Into: %s" % destTag['name']) - if srcTag: - lines.append("Moved From: %s" % srcTag['name']) - lines.append("Build: %s" % koji.buildLabel(build)) - lines.append("Tagged By: %s" % user['name']) - lines.append("Ignore Success?: %s" % (params[6] and 'yes' or 'no')) - if params[7]: - lines.append("Failure Message: %s" % params[7]) - elif method == 'dependantTask': - lines.append("Dependant Tasks: %s" % ", ".join([str(depID) for depID in params[0]])) - lines.append("Subtasks:") - for subtask in params[1]: - lines.append(" Method: %s" % subtask[0]) - lines.append(" Parameters: %s" % - ", ".join([str(subparam) for subparam in subtask[1]])) - if len(subtask) > 2 and subtask[2]: - subopts = subtask[2] - _handleOpts(lines, subopts, prefix=' ') - lines.append("") - elif method == 'chainbuild': - lines.append("Build Groups:") - group_num = 0 - for group_list in params[0]: - group_num += 1 - lines.append(" %i: %s" % (group_num, ', '.join(group_list))) - lines.append("Build Target: %s" % params[1]) - if len(params) > 2: - _handleOpts(lines, params[2]) - elif method == 'waitrepo': - lines.append("Build Target: %s" % params[0]) - if params[1]: - lines.append("Newer Than: %s" % params[1]) - if params[2]: - lines.append("NVRs: %s" % ', '.join(params[2])) - - return lines - - -def _printTaskInfo(session, task_id, topdir, level=0, recurse=True, verbose=True): - """Recursive function to print information about a task - and its children.""" - - BUILDDIR = '/var/lib/mock' - indent = " " * 2 * level - - info = session.getTaskInfo(task_id) - - if info is None: - raise koji.GenericError("No such task: %d" % task_id) - - if info['host_id']: - host_info = session.getHost(info['host_id']) - else: - host_info = None - buildroot_infos = session.listBuildroots(taskID=task_id) - build_info = session.listBuilds(taskID=task_id) - - files = list_task_output_all_volumes(session, task_id) - logs = [] - output = [] - for filename in files: - if filename.endswith('.log'): - logs += [os.path.join(koji.pathinfo.work(volume=volume), - koji.pathinfo.taskrelpath(task_id), - filename) for volume in files[filename]] - else: - output += [os.path.join(koji.pathinfo.work(volume=volume), - koji.pathinfo.taskrelpath(task_id), - filename) for volume in files[filename]] - - owner = session.getUser(info['owner'])['name'] - - print("%sTask: %d" % (indent, task_id)) - print("%sType: %s" % (indent, info['method'])) - if verbose: - print("%sRequest Parameters:" % indent) - for line in _parseTaskParams(session, info['method'], task_id, topdir): - print("%s %s" % (indent, line)) - print("%sOwner: %s" % (indent, owner)) - print("%sState: %s" % (indent, koji.TASK_STATES[info['state']].lower())) - print("%sCreated: %s" % (indent, time.asctime(time.localtime(info['create_ts'])))) - if info.get('start_ts'): - print("%sStarted: %s" % (indent, time.asctime(time.localtime(info['start_ts'])))) - if info.get('completion_ts'): - print("%sFinished: %s" % (indent, time.asctime(time.localtime(info['completion_ts'])))) - if host_info: - print("%sHost: %s" % (indent, host_info['name'])) - if build_info: - print("%sBuild: %s (%d)" % (indent, build_info[0]['nvr'], build_info[0]['build_id'])) - if buildroot_infos: - print("%sBuildroots:" % indent) - for root in buildroot_infos: - print("%s %s/%s-%d-%d/" % - (indent, BUILDDIR, root['tag_name'], root['id'], root['repo_id'])) - if logs: - print("%sLog Files:" % indent) - for log_path in logs: - print("%s %s" % (indent, log_path)) - if output: - print("%sOutput:" % indent) - for file_path in output: - print("%s %s" % (indent, file_path)) - - # white space - print('') - - if recurse: - level += 1 - children = session.getTaskChildren(task_id, request=True) - children.sort(key=lambda x: x['id']) - for child in children: - _printTaskInfo(session, child['id'], topdir, level, verbose=verbose) - - -def anon_handle_taskinfo(goptions, session, args): - """[info] Show information about a task""" - usage = "usage: %prog taskinfo [options] [ ...]" - parser = OptionParser(usage=get_usage_str(usage)) - parser.add_option("-r", "--recurse", action="store_true", - help="Show children of this task as well") - parser.add_option("-v", "--verbose", action="store_true", help="Be verbose") - (options, args) = parser.parse_args(args) - if len(args) < 1: - parser.error("You must specify at least one task ID") - - ensure_connection(session, goptions) - - for arg in args: - task_id = int(arg) - _printTaskInfo(session, task_id, goptions.topdir, 0, options.recurse, options.verbose) - - -def anon_handle_taginfo(goptions, session, args): - "[info] Print basic information about a tag" - usage = "usage: %prog taginfo [options] [ ...]" - parser = OptionParser(usage=get_usage_str(usage)) - parser.add_option("--event", type='int', metavar="EVENT#", help="query at event") - parser.add_option("--ts", type='int', metavar="TIMESTAMP", - help="query at last event before timestamp") - parser.add_option("--repo", type='int', metavar="REPO#", help="query at event for a repo") - (options, args) = parser.parse_args(args) - if len(args) < 1: - parser.error("Please specify a tag") - ensure_connection(session, goptions) - event = koji.util.eventFromOpts(session, options) - event_opts = {} - if event: - event['timestr'] = time.asctime(time.localtime(event['ts'])) - print("Querying at event %(id)i (%(timestr)s)" % event) - event_opts['event'] = event['id'] - perms = dict([(p['id'], p['name']) for p in session.getAllPerms()]) - - tags = [] - for tag in args: - info = session.getBuildConfig(tag, **event_opts) - if info is None: - try: - info = session.getBuildConfig(int(tag), **event_opts) - except ValueError: - info = None - if info is None: - parser.error('No such tag: %s' % tag) - tags.append(info) - - for n, info in enumerate(tags): - if n > 0: - print('') - print("Tag: %(name)s [%(id)d]" % info) - print("Arches: %(arches)s" % info) - group_list = sorted([x['name'] for x in session.getTagGroups(info['id'], **event_opts)]) - print("Groups: " + ', '.join(group_list)) - if info.get('locked'): - print('LOCKED') - if info.get('perm_id') is not None: - perm_id = info['perm_id'] - print("Required permission: %r" % perms.get(perm_id, perm_id)) - if session.mavenEnabled(): - print("Maven support?: %s" % (info['maven_support'] and 'yes' or 'no')) - print("Include all Maven archives?: %s" % - (info['maven_include_all'] and 'yes' or 'no')) - if 'extra' in info: - print("Tag options:") - for key in sorted(info['extra'].keys()): - line = " %s : %s" % (key, pprint.pformat(info['extra'][key])) - if key in info.get('config_inheritance', {}).get('extra', []): - line = "%-30s [%s]" % (line, info['config_inheritance']['extra'][key]['name']) - print(line) - dest_targets = session.getBuildTargets(destTagID=info['id'], **event_opts) - build_targets = session.getBuildTargets(buildTagID=info['id'], **event_opts) - repos = {} - if not event: - for target in dest_targets + build_targets: - if target['build_tag'] not in repos: - repo = session.getRepo(target['build_tag']) - if repo is None: - repos[target['build_tag']] = "no active repo" - else: - repos[target['build_tag']] = "repo#%(id)i: %(creation_time)s" % repo - if dest_targets: - print("Targets that build into this tag:") - for target in dest_targets: - if event: - print(" %s (%s)" % (target['name'], target['build_tag_name'])) - else: - print(" %s (%s, %s)" % - (target['name'], target['build_tag_name'], repos[target['build_tag']])) - if build_targets: - print("This tag is a buildroot for one or more targets") - if not event: - print("Current repo: %s" % repos[info['id']]) - print("Targets that build from this tag:") - for target in build_targets: - print(" %s" % target['name']) - external_repos = session.getTagExternalRepos(tag_info=info['id'], **event_opts) - if external_repos: - print("External repos:") - for rinfo in external_repos: - if 'arches' not in rinfo: - # older hubs will not return this field - rinfo['arches'] = '-' - elif not rinfo['arches']: - rinfo['arches'] = 'inherited from tag' - # TODO else intersection of arches? - print(" %(priority)3i %(external_repo_name)s " - "(%(url)s, merge mode: %(merge_mode)s), arches: %(arches)s" % rinfo) - print("Inheritance:") - for parent in session.getInheritanceData(info['id'], **event_opts): - parent['flags'] = format_inheritance_flags(parent) - print(" %(priority)-4d %(flags)s %(name)s [%(parent_id)s]" % parent) - if parent['maxdepth'] is not None: - print(" maxdepth: %(maxdepth)s" % parent) - if parent['pkg_filter']: - print(" package filter: %(pkg_filter)s" % parent) - - -def handle_add_tag(goptions, session, args): - "[admin] Add a new tag to the database" - usage = "usage: %prog add-tag [options] " - parser = OptionParser(usage=get_usage_str(usage)) - parser.add_option("--parent", help="Set a parent tag with priority 0") - parser.add_option("--arches", help="Specify arches") - parser.add_option("--maven-support", action="store_true", - help="Enable creation of Maven repos for this tag") - parser.add_option("--include-all", action="store_true", - help="Include all packages in this tag when generating Maven repos") - parser.add_option("-x", "--extra", action="append", default=[], metavar="key=value", - help="Set tag extra option") - (options, args) = parser.parse_args(args) - if len(args) != 1: - parser.error("Please specify a name for the tag") - activate_session(session, goptions) - if not (session.hasPerm('admin') or session.hasPerm('tag')): - parser.error("This action requires tag or admin privileges") - opts = {} - if options.parent: - opts['parent'] = options.parent - if options.arches: - opts['arches'] = koji.parse_arches(options.arches) - if options.maven_support: - opts['maven_support'] = True - if options.include_all: - opts['maven_include_all'] = True - if options.extra: - extra = {} - for xopt in options.extra: - key, value = xopt.split('=', 1) - value = arg_filter(value) - extra[key] = value - opts['extra'] = extra - session.createTag(args[0], **opts) - - -def handle_edit_tag(goptions, session, args): - "[admin] Alter tag information" - usage = "usage: %prog edit-tag [options] " - parser = OptionParser(usage=get_usage_str(usage)) - parser.add_option("--arches", help="Specify arches") - parser.add_option("--perm", help="Specify permission requirement") - parser.add_option("--no-perm", action="store_true", help="Remove permission requirement") - parser.add_option("--lock", action="store_true", help="Lock the tag") - parser.add_option("--unlock", action="store_true", help="Unlock the tag") - parser.add_option("--rename", help="Rename the tag") - parser.add_option("--maven-support", action="store_true", - help="Enable creation of Maven repos for this tag") - parser.add_option("--no-maven-support", action="store_true", - help="Disable creation of Maven repos for this tag") - parser.add_option("--include-all", action="store_true", - help="Include all packages in this tag when generating Maven repos") - parser.add_option("--no-include-all", action="store_true", - help="Do not include all packages in this tag when generating Maven repos") - parser.add_option("-x", "--extra", action="append", default=[], metavar="key=value", - help="Set tag extra option. JSON-encoded or simple value") - parser.add_option("-r", "--remove-extra", action="append", default=[], metavar="key", - help="Remove tag extra option") - parser.add_option("-b", "--block-extra", action="append", default=[], metavar="key", - help="Block inherited tag extra option") - (options, args) = parser.parse_args(args) - if len(args) != 1: - parser.error("Please specify a name for the tag") - activate_session(session, goptions) - tag = args[0] - opts = {} - if options.arches: - opts['arches'] = koji.parse_arches(options.arches) - if options.no_perm: - opts['perm'] = None - elif options.perm: - opts['perm'] = options.perm - if options.unlock: - opts['locked'] = False - if options.lock: - opts['locked'] = True - if options.rename: - opts['name'] = options.rename - if options.maven_support: - opts['maven_support'] = True - if options.no_maven_support: - opts['maven_support'] = False - if options.include_all: - opts['maven_include_all'] = True - if options.no_include_all: - opts['maven_include_all'] = False - if options.extra: - extra = {} - for xopt in options.extra: - key, value = xopt.split('=', 1) - if key in extra: - parser.error("Duplicate extra key: %s" % key) - extra[key] = arg_filter(value, parse_json=True) - opts['extra'] = extra - opts['remove_extra'] = options.remove_extra - opts['block_extra'] = options.block_extra - # XXX change callname - session.editTag2(tag, **opts) - - -def handle_lock_tag(goptions, session, args): - "[admin] Lock a tag" - usage = "usage: %prog lock-tag [options] [ ...]" - parser = OptionParser(usage=get_usage_str(usage)) - parser.add_option("--perm", help="Specify permission requirement") - parser.add_option("--glob", action="store_true", help="Treat args as glob patterns") - parser.add_option("--master", action="store_true", help="Lock the master lock") - parser.add_option("-n", "--test", action="store_true", help="Test mode") - (options, args) = parser.parse_args(args) - if len(args) < 1: - parser.error("Please specify a tag") - activate_session(session, goptions) - pdata = session.getAllPerms() - perm_ids = dict([(p['name'], p['id']) for p in pdata]) - perm = options.perm - if perm is None: - perm = 'admin' - perm_id = perm_ids[perm] - if options.glob: - selected = [] - for tag in session.listTags(): - for pattern in args: - if fnmatch.fnmatch(tag['name'], pattern): - selected.append(tag) - break - if not selected: - print("No tags matched") - else: - selected = [session.getTag(name, strict=True) for name in args] - for tag in selected: - if options.master: - # set the master lock - if tag['locked']: - print("Tag %s: master lock already set" % tag['name']) - continue - elif options.test: - print("Would have set master lock for: %s" % tag['name']) - continue - session.editTag2(tag['id'], locked=True) - else: - if tag['perm_id'] == perm_id: - print("Tag %s: %s permission already required" % (tag['name'], perm)) - continue - elif options.test: - print("Would have set permission requirement %s for tag %s" % (perm, tag['name'])) - continue - session.editTag2(tag['id'], perm_id=perm_id) - - -def handle_unlock_tag(goptions, session, args): - "[admin] Unlock a tag" - usage = "usage: %prog unlock-tag [options] [ ...]" - parser = OptionParser(usage=get_usage_str(usage)) - parser.add_option("--glob", action="store_true", help="Treat args as glob patterns") - parser.add_option("-n", "--test", action="store_true", help="Test mode") - (options, args) = parser.parse_args(args) - if len(args) < 1: - parser.error("Please specify a tag") - activate_session(session, goptions) - if options.glob: - selected = [] - for tag in session.listTags(): - for pattern in args: - if fnmatch.fnmatch(tag['name'], pattern): - selected.append(tag) - break - if not selected: - print("No tags matched") - else: - selected = [] - for name in args: - tag = session.getTag(name) - if tag is None: - parser.error("No such tag: %s" % name) - selected.append(tag) - for tag in selected: - opts = {} - if tag['locked']: - opts['locked'] = False - if tag['perm_id']: - opts['perm'] = None - if not opts: - print("Tag %(name)s: not locked" % tag) - continue - if options.test: - print("Tag %s: skipping changes: %r" % (tag['name'], opts)) - else: - session.editTag2(tag['id'], **opts) - - -def handle_add_tag_inheritance(goptions, session, args): - """[admin] Add to a tag's inheritance""" - usage = "usage: %prog add-tag-inheritance [options] " - parser = OptionParser(usage=get_usage_str(usage)) - parser.add_option("--priority", help="Specify priority") - parser.add_option("--maxdepth", help="Specify max depth") - parser.add_option("--intransitive", action="store_true", help="Set intransitive") - parser.add_option("--noconfig", action="store_true", help="Set to packages only") - parser.add_option("--pkg-filter", help="Specify the package filter") - parser.add_option("--force", action="store_true", - help="Force adding a parent to a tag that already has that parent tag") - (options, args) = parser.parse_args(args) - - if len(args) != 2: - parser.error("This command takes exctly two argument: a tag name or ID and that tag's " - "new parent name or ID") - - activate_session(session, goptions) - - tag = session.getTag(args[0]) - if not tag: - parser.error("No such tag: %s" % args[0]) - - parent = session.getTag(args[1]) - if not parent: - parser.error("No such tag: %s" % args[1]) - - inheritanceData = session.getInheritanceData(tag['id']) - priority = options.priority and int(options.priority) or 0 - sameParents = [datum for datum in inheritanceData if datum['parent_id'] == parent['id']] - samePriority = [datum for datum in inheritanceData if datum['priority'] == priority] - - if sameParents and not options.force: - warn("Error: You are attempting to add %s as %s's parent even though it already is " - "%s's parent." - % (parent['name'], tag['name'], tag['name'])) - error("Please use --force if this is what you really want to do.") - if samePriority: - error("Error: There is already an active inheritance with that priority on %s, " - "please specify a different priority with --priority." % tag['name']) - - new_data = {} - new_data['parent_id'] = parent['id'] - new_data['priority'] = options.priority or 0 - if options.maxdepth and options.maxdepth.isdigit(): - new_data['maxdepth'] = int(options.maxdepth) - else: - new_data['maxdepth'] = None - new_data['intransitive'] = options.intransitive or False - new_data['noconfig'] = options.noconfig or False - new_data['pkg_filter'] = options.pkg_filter or '' - - inheritanceData.append(new_data) - session.setInheritanceData(tag['id'], inheritanceData) - - -def handle_edit_tag_inheritance(goptions, session, args): - """[admin] Edit tag inheritance""" - usage = "usage: %prog edit-tag-inheritance [options] " - parser = OptionParser(usage=get_usage_str(usage)) - parser.add_option("--priority", help="Specify a new priority") - parser.add_option("--maxdepth", help="Specify max depth") - parser.add_option("--intransitive", action="store_true", help="Set intransitive") - parser.add_option("--noconfig", action="store_true", help="Set to packages only") - parser.add_option("--pkg-filter", help="Specify the package filter") - (options, args) = parser.parse_args(args) - - if len(args) < 1: - parser.error("This command takes at least one argument: a tag name or ID") - - if len(args) > 3: - parser.error("This command takes at most three argument: a tag name or ID, " - "a parent tag name or ID, and a priority") - - activate_session(session, goptions) - - tag = session.getTag(args[0]) - if not tag: - parser.error("No such tag: %s" % args[0]) - - parent = None - priority = None - if len(args) > 1: - parent = session.getTag(args[1]) - if not parent: - parser.error("No such tag: %s" % args[1]) - if len(args) > 2: - priority = args[2] - - data = session.getInheritanceData(tag['id']) - if parent and data: - data = [datum for datum in data if datum['parent_id'] == parent['id']] - if priority and data: - data = [datum for datum in data if datum['priority'] == priority] - - if len(data) == 0: - error("No inheritance link found to remove. Please check your arguments") - elif len(data) > 1: - print("Multiple matches for tag.") - if not parent: - error("Please specify a parent on the command line.") - if not priority: - error("Please specify a priority on the command line.") - error("Error: Key constraints may be broken. Exiting.") - - # len(data) == 1 - data = data[0] - - inheritanceData = session.getInheritanceData(tag['id']) - samePriority = [datum for datum in inheritanceData if datum['priority'] == options.priority] - if samePriority: - error("Error: There is already an active inheritance with that priority on %s, " - "please specify a different priority with --priority." % tag['name']) - - new_data = data.copy() - if options.priority is not None and options.priority.isdigit(): - new_data['priority'] = int(options.priority) - if options.maxdepth is not None: - if options.maxdepth.isdigit(): - new_data['maxdepth'] = int(options.maxdepth) - elif options.maxdepth.lower() == "none": - new_data['maxdepth'] = None - else: - error("Invalid maxdepth: %s" % options.maxdepth) - if options.intransitive: - new_data['intransitive'] = options.intransitive - if options.noconfig: - new_data['noconfig'] = options.noconfig - if options.pkg_filter: - new_data['pkg_filter'] = options.pkg_filter - - # find the data we want to edit and replace it - index = inheritanceData.index(data) - inheritanceData[index] = new_data - session.setInheritanceData(tag['id'], inheritanceData) - - -def handle_remove_tag_inheritance(goptions, session, args): - """[admin] Remove a tag inheritance link""" - usage = "usage: %prog remove-tag-inheritance " - parser = OptionParser(usage=get_usage_str(usage)) - (options, args) = parser.parse_args(args) - - if len(args) < 1: - parser.error("This command takes at least one argument: a tag name or ID") - - if len(args) > 3: - parser.error("This command takes at most three argument: a tag name or ID, a parent tag " - "name or ID, and a priority") - - activate_session(session, goptions) - - tag = session.getTag(args[0]) - if not tag: - parser.error("No such tag: %s" % args[0]) - - parent = None - priority = None - if len(args) > 1: - parent = session.getTag(args[1]) - if not parent: - parser.error("No such tag: %s" % args[1]) - if len(args) > 2: - priority = args[2] - - data = session.getInheritanceData(tag['id']) - if parent and data: - data = [datum for datum in data if datum['parent_id'] == parent['id']] - if priority and data: - data = [datum for datum in data if datum['priority'] == priority] - - if len(data) == 0: - error("No inheritance link found to remove. Please check your arguments") - elif len(data) > 1: - print("Multiple matches for tag.") - if not parent: - error("Please specify a parent on the command line.") - if not priority: - error("Please specify a priority on the command line.") - error("Error: Key constraints may be broken. Exiting.") - - # len(data) == 1 - data = data[0] - - inheritanceData = session.getInheritanceData(tag['id']) - - new_data = data.copy() - new_data['delete link'] = True - - # find the data we want to edit and replace it - index = inheritanceData.index(data) - inheritanceData[index] = new_data - session.setInheritanceData(tag['id'], inheritanceData) - - -def anon_handle_show_groups(goptions, session, args): - "[info] Show groups data for a tag" - usage = "usage: %prog show-groups [options] " - parser = OptionParser(usage=get_usage_str(usage)) - parser.add_option("--comps", action="store_true", help="Print in comps format") - parser.add_option("-x", "--expand", action="store_true", default=False, - help="Expand groups in comps format") - parser.add_option("--spec", action="store_true", help="Print build spec") - parser.add_option("--show-blocked", action="store_true", dest="incl_blocked", - help="Show blocked packages") - (options, args) = parser.parse_args(args) - if len(args) != 1: - parser.error("Incorrect number of arguments") - if options.incl_blocked and (options.comps or options.spec): - parser.error("--show-blocked doesn't make sense for comps/spec output") - ensure_connection(session, goptions) - tag = args[0] - callopts = {} - if options.incl_blocked: - callopts['incl_blocked'] = True - groups = session.getTagGroups(tag, **callopts) - if options.comps: - print(koji.generate_comps(groups, expand_groups=options.expand)) - elif options.spec: - print(koji.make_groups_spec(groups, name='buildgroups', buildgroup='build')) - else: - pprint.pprint(groups) - - -def anon_handle_list_external_repos(goptions, session, args): - "[info] List external repos" - usage = "usage: %prog list-external-repos [options]" - parser = OptionParser(usage=get_usage_str(usage)) - parser.add_option("--url", help="Select by url") - parser.add_option("--name", help="Select by name") - parser.add_option("--id", type="int", help="Select by id") - parser.add_option("--tag", help="Select by tag") - parser.add_option("--used", action='store_true', help="List which tags use the repo(s)") - parser.add_option("--inherit", action='store_true', - help="Follow tag inheritance when selecting by tag") - parser.add_option("--event", type='int', metavar="EVENT#", help="Query at event") - parser.add_option("--ts", type='int', metavar="TIMESTAMP", - help="Query at last event before timestamp") - parser.add_option("--repo", type='int', metavar="REPO#", - help="Query at event corresponding to (nonexternal) repo") - parser.add_option("--quiet", action="store_true", default=goptions.quiet, - help="Do not display the column headers") - (options, args) = parser.parse_args(args) - if len(args) > 0: - parser.error("This command takes no arguments") - ensure_connection(session, goptions) - opts = {} - event = koji.util.eventFromOpts(session, options) - if event: - opts['event'] = event['id'] - event['timestr'] = time.asctime(time.localtime(event['ts'])) - print("Querying at event %(id)i (%(timestr)s)" % event) - if options.tag: - format = "tag" - opts['tag_info'] = options.tag - opts['repo_info'] = options.id or options.name or None - if opts['repo_info']: - if options.inherit: - parser.error("Can't select by repo when using --inherit") - if options.inherit: - del opts['repo_info'] - data = session.getExternalRepoList(**opts) - format = "multitag" - else: - data = session.getTagExternalRepos(**opts) - elif options.used: - format = "multitag" - opts['repo_info'] = options.id or options.name or None - data = session.getTagExternalRepos(**opts) - else: - format = "basic" - opts['info'] = options.id or options.name or None - opts['url'] = options.url or None - data = session.listExternalRepos(**opts) - - # There are three different output formats - # 1) Listing just repo data (name, url) - # 2) Listing repo data for a tag (priority, name, url) - # 3) Listing repo data for multiple tags (tag, priority, name, url) - if format == "basic": - format = "%(name)-25s %(url)s" - header1 = "%-25s %s" % ("External repo name", "URL") - header2 = "%s %s" % ("-" * 25, "-" * 40) - elif format == "tag": - format = "%(priority)-3i %(external_repo_name)-25s %(merge_mode)-10s %(url)s" - header1 = "%-3s %-25s %-10s URL" % ("Pri", "External repo name", "Mode") - header2 = "%s %s %s %s" % ("-" * 3, "-" * 25, "-" * 10, "-" * 40) - elif format == "multitag": - format = "%(tag_name)-20s %(priority)-3i %(merge_mode)-10s %(external_repo_name)s" - header1 = "%-20s %-3s %-10s %s" % ("Tag", "Pri", "Mode", "External repo name") - header2 = "%s %s %s %s" % ("-" * 20, "-" * 3, "-" * 10, "-" * 25) - if not options.quiet: - print(header1) - print(header2) - for rinfo in data: - # older hubs do not support merge_mode - rinfo.setdefault('merge_mode', None) - print(format % rinfo) - - -def _pick_external_repo_priority(session, tag): - """pick priority after current ones, leaving space for later insertions""" - repolist = session.getTagExternalRepos(tag_info=tag) - # ordered by priority - if not repolist: - priority = 5 - else: - priority = (repolist[-1]['priority'] + 7) // 5 * 5 - # at least 3 higher than current max and a multiple of 5 - return priority - - -def _parse_tagpri(tagpri): - parts = tagpri.rsplit('::', 1) - tag = parts[0] - if len(parts) == 1: - return tag, None - elif parts[1] in ('auto', '-1'): - return tag, None - else: - try: - pri = int(parts[1]) - except ValueError: - raise koji.GenericError("Invalid priority: %s" % parts[1]) - return tag, pri - - -def handle_add_external_repo(goptions, session, args): - "[admin] Create an external repo and/or add one to a tag" - usage = "usage: %prog add-external-repo [options] []" - parser = OptionParser(usage=get_usage_str(usage)) - parser.add_option("-t", "--tag", action="append", metavar="TAG", - help="Also add repo to tag. Use tag::N to set priority") - parser.add_option("-p", "--priority", type='int', - help="Set priority (when adding to tag)") - parser.add_option("-m", "--mode", help="Set merge mode") - parser.add_option("-a", "--arches", metavar="ARCH1,ARCH2, ...", - help="Use only subset of arches from given repo") - (options, args) = parser.parse_args(args) - activate_session(session, goptions) - if options.mode: - if options.mode not in koji.REPO_MERGE_MODES: - parser.error('Invalid mode: %s' % options.mode) - if not options.tag: - parser.error('The --mode option can only be used with --tag') - if len(args) == 1: - name = args[0] - rinfo = session.getExternalRepo(name, strict=True) - if not options.tag: - parser.error("A url is required to create an external repo entry") - elif len(args) == 2: - name, url = args - rinfo = session.createExternalRepo(name, url) - print("Created external repo %(id)i" % rinfo) - else: - parser.error("Incorrect number of arguments") - if options.tag: - for tagpri in options.tag: - tag, priority = _parse_tagpri(tagpri) - if priority is None: - if options.priority is not None: - priority = options.priority - else: - priority = _pick_external_repo_priority(session, tag) - callopts = {} - if options.mode: - callopts['merge_mode'] = options.mode - if options.arches: - callopts['arches'] = options.arches - session.addExternalRepoToTag(tag, rinfo['name'], priority, **callopts) - print("Added external repo %s to tag %s (priority %i)" - % (rinfo['name'], tag, priority)) - - -def handle_edit_external_repo(goptions, session, args): - "[admin] Edit data for an external repo" - usage = "usage: %prog edit-external-repo [options] " - parser = OptionParser(usage=get_usage_str(usage)) - parser.add_option("--url", help="Change the url") - parser.add_option("--name", help="Change the name") - parser.add_option("-t", "--tag", metavar="TAG", help="Edit the repo properties for the tag.") - parser.add_option("-p", "--priority", metavar="PRIORITY", type='int', - help="Edit the priority of the repo for the tag specified by --tag.") - parser.add_option("-m", "--mode", metavar="MODE", - help="Edit the merge mode of the repo for the tag specified by --tag. " - "Options: %s." % ", ".join(koji.REPO_MERGE_MODES)) - parser.add_option("-a", "--arches", metavar="ARCH1,ARCH2, ...", - help="Use only subset of arches from given repo") - (options, args) = parser.parse_args(args) - if len(args) != 1: - parser.error("Incorrect number of arguments") - repo_opts = {} - if options.url: - repo_opts['url'] = options.url - if options.name: - repo_opts['name'] = options.name - tag_repo_opts = {} - if options.tag: - if options.priority is not None: - tag_repo_opts['priority'] = options.priority - if options.mode: - tag_repo_opts['merge_mode'] = options.mode - if options.arches is not None: - tag_repo_opts['arches'] = options.arches - if not tag_repo_opts: - parser.error("At least, one of priority and merge mode should be specified") - tag_repo_opts['tag_info'] = options.tag - tag_repo_opts['repo_info'] = args[0] - else: - for k in ('priority', 'mode', 'arches'): - if getattr(options, k) is not None: - parser.error("If %s is specified, --tag must be specified as well" % k) - - if not (repo_opts or tag_repo_opts): - parser.error("No changes specified") - - activate_session(session, goptions) - if repo_opts: - session.editExternalRepo(args[0], **repo_opts) - if tag_repo_opts: - session.editTagExternalRepo(**tag_repo_opts) - - -def handle_remove_external_repo(goptions, session, args): - "[admin] Remove an external repo from a tag or tags, or remove entirely" - usage = "usage: %prog remove-external-repo [ ...]" - parser = OptionParser(usage=get_usage_str(usage)) - parser.add_option("--alltags", action="store_true", help="Remove from all tags") - parser.add_option("--force", action='store_true', help="Force action") - (options, args) = parser.parse_args(args) - if len(args) < 1: - parser.error("Incorrect number of arguments") - activate_session(session, goptions) - repo = args[0] - tags = args[1:] - delete = not bool(tags) - data = session.getTagExternalRepos(repo_info=repo) - current_tags = [d['tag_name'] for d in data] - if options.alltags: - delete = False - if tags: - parser.error("Do not specify tags when using --alltags") - if not current_tags: - if options.force: - delete = True - else: - warn("External repo %s not associated with any tags" % repo) - return 0 - tags = current_tags - if delete: - # removing entirely - if current_tags and not options.force: - warn("Error: external repo %s used by tag(s): %s" % (repo, ', '.join(current_tags))) - error("Use --force to remove anyway") - session.deleteExternalRepo(args[0]) - else: - for tag in tags: - if tag not in current_tags: - print("External repo %s not associated with tag %s" % (repo, tag)) - continue - session.removeExternalRepoFromTag(tag, repo) - - -# This handler is for spinning livecd images -# -def handle_spin_livecd(options, session, args): - """[build] Create a live CD image given a kickstart file""" - - # Usage & option parsing. - usage = "usage: %prog spin-livecd [options] " - parser = OptionParser(usage=get_usage_str(usage)) - parser.add_option("--wait", action="store_true", - help="Wait on the livecd creation, even if running in the background") - parser.add_option("--nowait", action="store_false", dest="wait", - help="Don't wait on livecd creation") - parser.add_option("--noprogress", action="store_true", - help="Do not display progress of the upload") - parser.add_option("--background", action="store_true", - help="Run the livecd creation task at a lower priority") - parser.add_option("--ksurl", metavar="SCMURL", - help="The URL to the SCM containing the kickstart file") - parser.add_option("--ksversion", metavar="VERSION", - help="The syntax version used in the kickstart file") - parser.add_option("--scratch", action="store_true", - help="Create a scratch LiveCD image") - parser.add_option("--repo", action="append", - help="Specify a repo that will override the repo used to install " - "RPMs in the LiveCD. May be used multiple times. The " - "build tag repo associated with the target is the default.") - parser.add_option("--release", help="Forcibly set the release field") - parser.add_option("--volid", help="Set the volume id") - parser.add_option("--specfile", metavar="URL", - help="SCM URL to spec file fragment to use to generate wrapper RPMs") - parser.add_option("--skip-tag", action="store_true", - help="Do not attempt to tag package") - (task_options, args) = parser.parse_args(args) - - # Make sure the target and kickstart is specified. - print('spin-livecd is deprecated and will be replaced with spin-livemedia') - if len(args) != 5: - parser.error("Five arguments are required: a name, a version, an architecture, " - "a build target, and a relative path to a kickstart file.") - if task_options.volid is not None and len(task_options.volid) > 32: - parser.error('Volume ID has a maximum length of 32 characters') - return _build_image(options, task_options, session, args, 'livecd') - - -# This handler is for spinning livemedia images -def handle_spin_livemedia(options, session, args): - """[build] Create a livemedia image given a kickstart file""" - - # Usage & option parsing. - usage = "usage: %prog spin-livemedia [options] " \ - "" - parser = OptionParser(usage=get_usage_str(usage)) - parser.add_option("--wait", action="store_true", - help="Wait on the livemedia creation, even if running in the background") - parser.add_option("--nowait", action="store_false", dest="wait", - help="Don't wait on livemedia creation") - parser.add_option("--noprogress", action="store_true", - help="Do not display progress of the upload") - parser.add_option("--background", action="store_true", - help="Run the livemedia creation task at a lower priority") - parser.add_option("--ksurl", metavar="SCMURL", - help="The URL to the SCM containing the kickstart file") - parser.add_option("--install-tree-url", metavar="URL", - help="Provide the URL for the install tree") - parser.add_option("--ksversion", metavar="VERSION", - help="The syntax version used in the kickstart file") - parser.add_option("--scratch", action="store_true", - help="Create a scratch LiveMedia image") - parser.add_option("--repo", action="append", - help="Specify a repo that will override the repo used to install " - "RPMs in the LiveMedia. May be used multiple times. The " - "build tag repo associated with the target is the default.") - parser.add_option("--release", help="Forcibly set the release field") - parser.add_option("--volid", help="Set the volume id") - parser.add_option("--specfile", metavar="URL", - help="SCM URL to spec file fragment to use to generate wrapper RPMs") - parser.add_option("--skip-tag", action="store_true", help="Do not attempt to tag package") - parser.add_option("--can-fail", action="store", dest="optional_arches", - metavar="ARCH1,ARCH2,...", default="", - help="List of archs which are not blocking for build (separated by commas.") - parser.add_option('--lorax_dir', metavar='DIR', - help='The relative path to the lorax templates ' - 'directory within the checkout of "lorax_url".') - parser.add_option('--lorax_url', metavar='URL', - help='The URL to the SCM containing any custom lorax templates that are ' - 'to be used to override the default templates.') - parser.add_option('--nomacboot', action="store_true", - help="Pass the nomacboot option to livemedia-creator") - parser.add_option('--ksrepo', action="store_true", - help="Do not overwrite repos in the kickstart") - parser.add_option('--squashfs-only', action="store_true", - help="Use a plain squashfs filesystem.") - parser.add_option('--compress-arg', action="append", default=[], metavar="ARG OPT", - help="List of compressions.") - (task_options, args) = parser.parse_args(args) - - # Make sure the target and kickstart is specified. - if len(args) != 5: - parser.error("Five arguments are required: a name, a version, a build target, " - "an architecture, and a relative path to a kickstart file.") - if task_options.lorax_url is not None and task_options.lorax_dir is None: - parser.error('The "--lorax_url" option requires that "--lorax_dir" also be used.') - if task_options.volid is not None and len(task_options.volid) > 32: - parser.error('Volume ID has a maximum length of 32 characters') - return _build_image(options, task_options, session, args, 'livemedia') - - -# This handler is for spinning appliance images -# -def handle_spin_appliance(options, session, args): - """[build] Create an appliance given a kickstart file""" - - # Usage & option parsing - usage = "usage: %prog spin-appliance [options] " \ - "" - parser = OptionParser(usage=get_usage_str(usage)) - parser.add_option("--wait", action="store_true", - help="Wait on the appliance creation, even if running in the background") - parser.add_option("--nowait", action="store_false", dest="wait", - help="Don't wait on appliance creation") - parser.add_option("--noprogress", action="store_true", - help="Do not display progress of the upload") - parser.add_option("--background", action="store_true", - help="Run the appliance creation task at a lower priority") - parser.add_option("--ksurl", metavar="SCMURL", - help="The URL to the SCM containing the kickstart file") - parser.add_option("--ksversion", metavar="VERSION", - help="The syntax version used in the kickstart file") - parser.add_option("--scratch", action="store_true", help="Create a scratch appliance") - parser.add_option("--repo", action="append", - help="Specify a repo that will override the repo used to install " - "RPMs in the appliance. May be used multiple times. The " - "build tag repo associated with the target is the default.") - parser.add_option("--release", help="Forcibly set the release field") - parser.add_option("--specfile", metavar="URL", - help="SCM URL to spec file fragment to use to generate wrapper RPMs") - parser.add_option("--skip-tag", action="store_true", help="Do not attempt to tag package") - parser.add_option("--vmem", metavar="VMEM", default=None, - help="Set the amount of virtual memory in the appliance in MB, " - "default is 512") - parser.add_option("--vcpu", metavar="VCPU", default=None, - help="Set the number of virtual cpus in the appliance, default is 1") - parser.add_option("--format", metavar="DISK_FORMAT", default='raw', - help="Disk format, default is raw. Other options are qcow, qcow2, and vmx.") - - (task_options, args) = parser.parse_args(args) - - # Make sure the target and kickstart is specified. - print('spin-appliance is deprecated and will be replaced with image-build') - if len(args) != 5: - parser.error("Five arguments are required: a name, a version, an architecture, " - "a build target, and a relative path to a kickstart file.") - return _build_image(options, task_options, session, args, 'appliance') - - -def handle_image_build_indirection(options, session, args): - """[build] Create a disk image using other disk images via the Indirection plugin""" - usage = "usage: %prog image-build-indirection [base_image] [utility_image] " \ - "[indirection_build_template]" - usage += "\n %prog image-build --config \n" - parser = OptionParser(usage=get_usage_str(usage)) - parser.add_option("--config", - help="Use a configuration file to define image-build options " - "instead of command line options (they will be ignored).") - parser.add_option("--background", action="store_true", - help="Run the image creation task at a lower priority") - parser.add_option("--name", help="Name of the output image") - parser.add_option("--version", help="Version of the output image") - parser.add_option("--release", help="Release of the output image") - parser.add_option("--arch", help="Architecture of the output image and input images") - parser.add_option("--target", help="Build target to use for the indirection build") - parser.add_option("--skip-tag", action="store_true", help="Do not tag the resulting build") - parser.add_option("--base-image-task", - help="ID of the createImage task of the base image to be used") - parser.add_option("--base-image-build", help="NVR or build ID of the base image to be used") - parser.add_option("--utility-image-task", - help="ID of the createImage task of the utility image to be used") - parser.add_option("--utility-image-build", - help="NVR or build ID of the utility image to be used") - parser.add_option("--indirection-template", - help="Name of the local file, or SCM file containing the template used to " - "drive the indirection plugin") - parser.add_option("--indirection-template-url", - help="SCM URL containing the template used to drive the indirection plugin") - parser.add_option("--results-loc", - help="Relative path inside the working space image where the results " - "should be extracted from") - parser.add_option("--scratch", action="store_true", help="Create a scratch image") - parser.add_option("--wait", action="store_true", - help="Wait on the image creation, even if running in the background") - parser.add_option("--nowait", action="store_false", dest="wait", - help="Do not wait on the image creation") - parser.add_option("--noprogress", action="store_true", - help="Do not display progress of the upload") - - (task_options, args) = parser.parse_args(args) - _build_image_indirection(options, task_options, session, args) - - -def _build_image_indirection(options, task_opts, session, args): - """ - A private helper function for builds using the indirection plugin of ImageFactory - """ - - # Do some sanity checks before even attempting to create the session - if not (bool(task_opts.utility_image_task) != - bool(task_opts.utility_image_build)): - raise koji.GenericError("You must specify either a utility-image task or build ID/NVR") - - if not (bool(task_opts.base_image_task) != - bool(task_opts.base_image_build)): - raise koji.GenericError("You must specify either a base-image task or build ID/NVR") - - required_opts = ['name', 'version', 'arch', 'target', 'indirection_template', 'results_loc'] - optional_opts = ['indirection_template_url', 'scratch', 'utility_image_task', - 'utility_image_build', 'base_image_task', 'base_image_build', 'release', - 'skip_tag'] - - missing = [] - for opt in required_opts: - if not getattr(task_opts, opt, None): - missing.append(opt) - - if len(missing) > 0: - print("Missing the following required options: %s" % - ' '.join(['--%s' % o.replace('_', '-') for o in missing])) - raise koji.GenericError("Missing required options specified above") - - activate_session(session, options) - - # Set the task's priority. Users can only lower it with --background. - priority = None - if task_opts.background: - # relative to koji.PRIO_DEFAULT; higher means a "lower" priority. - priority = 5 - if _running_in_bg() or task_opts.noprogress: - callback = None - else: - callback = _progress_callback - - # We do some early sanity checking of the given target. - # Kojid gets these values again later on, but we check now as a convenience - # for the user. - - tmp_target = session.getBuildTarget(task_opts.target) - if not tmp_target: - raise koji.GenericError("No such build target: %s" % tmp_target) - dest_tag = session.getTag(tmp_target['dest_tag']) - if not dest_tag: - raise koji.GenericError("No such destination tag: %s" % tmp_target['dest_tag_name']) - - # Set the architecture - task_opts.arch = koji.canonArch(task_opts.arch) - - # Upload the indirection template file to the staging area. - # If it's a URL, it's kojid's job to go get it when it does the checkout. - if not task_opts.indirection_template_url: - if not task_opts.scratch: - # only scratch builds can omit indirection_template_url - raise koji.GenericError( - "Non-scratch builds must provide a URL for the indirection template") - templatefile = task_opts.indirection_template - serverdir = unique_path('cli-image-indirection') - session.uploadWrapper(templatefile, serverdir, callback=callback) - task_opts.indirection_template = os.path.join('work', serverdir, - os.path.basename(templatefile)) - print('') - - hub_opts = {} - # Just pass everything in as opts. No posiitonal arguments at all. Why not? - for opt in required_opts + optional_opts: - val = getattr(task_opts, opt, None) - # We pass these through even if they are None - # The builder code can then check if they are set without using getattr - hub_opts[opt] = val - - # finally, create the task. - task_id = session.buildImageIndirection(opts=hub_opts, - priority=priority) - - if not options.quiet: - print("Created task: %d" % task_id) - print("Task info: %s/taskinfo?taskID=%s" % (options.weburl, task_id)) - if task_opts.wait or (task_opts.wait is None and not _running_in_bg()): - session.logout() - return watch_tasks(session, [task_id], quiet=options.quiet, topurl=options.topurl) - - -def handle_image_build(options, session, args): - """[build] Create a disk image given an install tree""" - formats = ('vmdk', 'qcow', 'qcow2', 'vdi', 'vpc', 'rhevm-ova', - 'vsphere-ova', 'vagrant-virtualbox', 'vagrant-libvirt', - 'vagrant-vmware-fusion', 'vagrant-hyperv', 'docker', 'raw-xz', - 'liveimg-squashfs', 'tar-gz') - usage = "usage: %prog image-build [options] " \ - " [ ...]" - usage += "\n %prog image-build --config \n" - parser = OptionParser(usage=get_usage_str(usage)) - parser.add_option("--background", action="store_true", - help="Run the image creation task at a lower priority") - parser.add_option("--config", - help="Use a configuration file to define image-build options " - "instead of command line options (they will be ignored).") - parser.add_option("--disk-size", default=10, help="Set the disk device size in gigabytes") - parser.add_option("--distro", - help="specify the RPM based distribution the image will be based " - "on with the format RHEL-X.Y, CentOS-X.Y, SL-X.Y, or Fedora-NN. " - "The packages for the Distro you choose must have been built " - "in this system.") - parser.add_option("--format", default=[], action="append", - help="Convert results to one or more formats " - "(%s), this option may be used " - "multiple times. By default, specifying this option will " - "omit the raw disk image (which is 10G in size) from the " - "build results. If you really want it included with converted " - "images, pass in 'raw' as an option." % ', '.join(formats)) - parser.add_option("--kickstart", help="Path to a local kickstart file") - parser.add_option("--ksurl", metavar="SCMURL", - help="The URL to the SCM containing the kickstart file") - parser.add_option("--ksversion", metavar="VERSION", - help="The syntax version used in the kickstart file") - parser.add_option("--noprogress", action="store_true", - help="Do not display progress of the upload") - parser.add_option("--noverifyssl", action="store_true", - help="Use the noverifyssl option for the install tree and all repos. " - "This option is only allowed if enabled on the builder.") - parser.add_option("--nowait", action="store_false", dest="wait", - help="Don't wait on image creation") - parser.add_option("--ova-option", action="append", - help="Override a value in the OVA description XML. Provide a value " - "in a name=value format, such as 'ovf_memory_mb=6144'") - parser.add_option("--factory-parameter", nargs=2, action="append", - help="Pass a parameter to Image Factory. The results are highly specific " - "to the image format being created. This is a two argument parameter " - "that can be specified an arbitrary number of times. For example: " - "--factory-parameter docker_cmd '[ \"/bin/echo Hello World\" ]'") - parser.add_option("--release", help="Forcibly set the release field") - parser.add_option("--repo", action="append", - help="Specify a repo that will override the repo used to install " - "RPMs in the image. May be used multiple times. The " - "build tag repo associated with the target is the default.") - parser.add_option("--scratch", action="store_true", help="Create a scratch image") - parser.add_option("--skip-tag", action="store_true", help="Do not attempt to tag package") - parser.add_option("--can-fail", action="store", dest="optional_arches", - metavar="ARCH1,ARCH2,...", default="", - help="List of archs which are not blocking for build (separated by commas.") - parser.add_option("--specfile", metavar="URL", - help="SCM URL to spec file fragment to use to generate wrapper RPMs") - parser.add_option("--wait", action="store_true", - help="Wait on the image creation, even if running in the background") - - (task_options, args) = parser.parse_args(args) - - if task_options.config: - section = 'image-build' - config = koji.read_config_files([(task_options.config, True)]) - if not config.has_section(section): - parser.error("single section called [%s] is required" % section) - # pluck out the positional arguments first - args = [] - for arg in ('name', 'version', 'target', 'install_tree'): - args.append(config.get(section, arg)) - config.remove_option(section, arg) - args.extend(config.get(section, 'arches').split(',')) - config.remove_option(section, 'arches') - # turn comma-separated options into lists - for arg in ('repo', 'format'): - if config.has_option(section, arg): - setattr(task_options, arg, config.get(section, arg).split(',')) - config.remove_option(section, arg) - if config.has_option(section, 'can_fail'): - setattr(task_options, 'optional_arches', config.get(section, 'can_fail').split(',')) - config.remove_option(section, 'can_fail') - # handle everything else - for k, v in config.items(section): - setattr(task_options, k, v) - - # ova-options belong in their own section - section = 'ova-options' - if config.has_section(section): - task_options.ova_option = [] - for k, v in config.items(section): - task_options.ova_option.append('%s=%s' % (k, v)) - - # as do factory-parameters - section = 'factory-parameters' - if config.has_section(section): - task_options.factory_parameter = [] - for k, v in config.items(section): - # We do this, rather than a dict, to match what argparse spits out - task_options.factory_parameter.append((k, v)) - - else: - if len(args) < 5: - parser.error("At least five arguments are required: a name, a version, " - "a build target, a URL to an install tree, and 1 or more architectures.") - if not task_options.ksurl and not task_options.kickstart: - parser.error('You must specify --kickstart') - if not task_options.distro: - parser.error( - "You must specify --distro. Examples: Fedora-16, RHEL-6.4, SL-6.4 or CentOS-6.4") - return _build_image_oz(options, task_options, session, args) - - -def _build_image(options, task_opts, session, args, img_type): - """ - A private helper function that houses common CLI code for building - images with chroot-based tools. - """ - - if img_type not in ('livecd', 'appliance', 'livemedia'): - raise koji.GenericError('Unrecognized image type: %s' % img_type) - activate_session(session, options) - - # Set the task's priority. Users can only lower it with --background. - priority = None - if task_opts.background: - # relative to koji.PRIO_DEFAULT; higher means a "lower" priority. - priority = 5 - if _running_in_bg() or task_opts.noprogress: - callback = None - else: - callback = _progress_callback - - # We do some early sanity checking of the given target. - # Kojid gets these values again later on, but we check now as a convenience - # for the user. - target = args[2] - tmp_target = session.getBuildTarget(target) - if not tmp_target: - raise koji.GenericError("No such build target: %s" % target) - dest_tag = session.getTag(tmp_target['dest_tag']) - if not dest_tag: - raise koji.GenericError("No such destination tag: %s" % tmp_target['dest_tag_name']) - - # Set the architecture - if img_type == 'livemedia': - # livemedia accepts multiple arches - arch = [koji.canonArch(a) for a in args[3].split(",")] - else: - arch = koji.canonArch(args[3]) - - # Upload the KS file to the staging area. - # If it's a URL, it's kojid's job to go get it when it does the checkout. - ksfile = args[4] - - if not task_opts.ksurl: - serverdir = unique_path('cli-' + img_type) - session.uploadWrapper(ksfile, serverdir, callback=callback) - ksfile = os.path.join(serverdir, os.path.basename(ksfile)) - print('') - - hub_opts = {} - passthru_opts = [ - 'format', 'install_tree_url', 'isoname', 'ksurl', - 'ksversion', 'release', 'repo', 'scratch', 'skip_tag', - 'specfile', 'vcpu', 'vmem', 'volid', 'optional_arches', - 'lorax_dir', 'lorax_url', 'nomacboot', 'ksrepo', - 'squashfs_only', 'compress_arg', - ] - for opt in passthru_opts: - val = getattr(task_opts, opt, None) - if val is not None: - hub_opts[opt] = val - - if 'optional_arches' in hub_opts: - hub_opts['optional_arches'] = hub_opts['optional_arches'].split(',') - # finally, create the task. - task_id = session.buildImage(args[0], args[1], arch, target, ksfile, - img_type, opts=hub_opts, priority=priority) - - if not options.quiet: - print("Created task: %d" % task_id) - print("Task info: %s/taskinfo?taskID=%s" % (options.weburl, task_id)) - if task_opts.wait or (task_opts.wait is None and not _running_in_bg()): - session.logout() - return watch_tasks(session, [task_id], quiet=options.quiet, - poll_interval=options.poll_interval, topurl=options.topurl) - - -def _build_image_oz(options, task_opts, session, args): - """ - A private helper function that houses common CLI code for building - images with Oz and ImageFactory - """ - activate_session(session, options) - - # Set the task's priority. Users can only lower it with --background. - priority = None - if task_opts.background: - # relative to koji.PRIO_DEFAULT; higher means a "lower" priority. - priority = 5 - if _running_in_bg() or task_opts.noprogress: - callback = None - else: - callback = _progress_callback - - # We do some early sanity checking of the given target. - # Kojid gets these values again later on, but we check now as a convenience - # for the user. - target = args[2] - tmp_target = session.getBuildTarget(target) - if not tmp_target: - raise koji.GenericError("No such build target: %s" % target) - dest_tag = session.getTag(tmp_target['dest_tag']) - if not dest_tag: - raise koji.GenericError("No such destination tag: %s" % tmp_target['dest_tag_name']) - - # Set the architectures - arches = [] - for arch in args[4:]: - arches.append(koji.canonArch(arch)) - - # Upload the KS file to the staging area. - # If it's a URL, it's kojid's job to go get it when it does the checkout. - if not task_opts.ksurl: - if not task_opts.scratch: - # only scratch builds can omit ksurl - raise koji.GenericError("Non-scratch builds must provide ksurl") - ksfile = task_opts.kickstart - serverdir = unique_path('cli-image') - session.uploadWrapper(ksfile, serverdir, callback=callback) - task_opts.kickstart = os.path.join('work', serverdir, - os.path.basename(ksfile)) - print('') - - hub_opts = {} - for opt in ('ksurl', 'ksversion', 'kickstart', 'scratch', 'repo', - 'release', 'skip_tag', 'specfile', 'distro', 'format', - 'disk_size', 'ova_option', 'factory_parameter', - 'optional_arches', 'noverifyssl'): - val = getattr(task_opts, opt, None) - if val is not None: - hub_opts[opt] = val - # finally, create the task. - task_id = session.buildImageOz(args[0], args[1], arches, target, args[3], - opts=hub_opts, priority=priority) - - if not options.quiet: - print("Created task: %d" % task_id) - print("Task info: %s/taskinfo?taskID=%s" % (options.weburl, task_id)) - if task_opts.wait or (task_opts.wait is None and not _running_in_bg()): - session.logout() - return watch_tasks(session, [task_id], quiet=options.quiet, - poll_interval=options.poll_interval, topurl=options.topurl) - - -def handle_win_build(options, session, args): - """[build] Build a Windows package from source""" - # Usage & option parsing - usage = "usage: %prog win-build [options] " - parser = OptionParser(usage=get_usage_str(usage)) - parser.add_option("--winspec", metavar="URL", - help="SCM URL to retrieve the build descriptor from. " - "If not specified, the winspec must be in the root directory " - "of the source repository.") - parser.add_option("--patches", metavar="URL", - help="SCM URL of a directory containing patches to apply " - "to the sources before building") - parser.add_option("--cpus", type="int", - help="Number of cpus to allocate to the build VM " - "(requires admin access)") - parser.add_option("--mem", type="int", - help="Amount of memory (in megabytes) to allocate to the build VM " - "(requires admin access)") - parser.add_option("--static-mac", action="store_true", - help="Retain the original MAC address when cloning the VM") - parser.add_option("--specfile", metavar="URL", - help="SCM URL of a spec file fragment to use to generate wrapper RPMs") - parser.add_option("--scratch", action="store_true", - help="Perform a scratch build") - parser.add_option("--repo-id", type="int", help="Use a specific repo") - parser.add_option("--skip-tag", action="store_true", help="Do not attempt to tag package") - parser.add_option("--background", action="store_true", - help="Run the build at a lower priority") - parser.add_option("--wait", action="store_true", - help="Wait on the build, even if running in the background") - parser.add_option("--nowait", action="store_false", dest="wait", help="Don't wait on build") - parser.add_option("--quiet", action="store_true", - help="Do not print the task information", default=options.quiet) - (build_opts, args) = parser.parse_args(args) - if len(args) != 3: - parser.error( - "Exactly three arguments (a build target, a SCM URL, and a VM name) are required") - activate_session(session, options) - target = args[0] - if target.lower() == "none" and build_opts.repo_id: - target = None - build_opts.skip_tag = True - else: - build_target = session.getBuildTarget(target) - if not build_target: - parser.error("No such build target: %s" % target) - dest_tag = session.getTag(build_target['dest_tag']) - if not dest_tag: - parser.error("No such destination tag: %s" % build_target['dest_tag_name']) - if dest_tag['locked'] and not build_opts.scratch: - parser.error("Destination tag %s is locked" % dest_tag['name']) - scmurl = args[1] - vm_name = args[2] - opts = {} - for key in ('winspec', 'patches', 'cpus', 'mem', 'static_mac', - 'specfile', 'scratch', 'repo_id', 'skip_tag'): - val = getattr(build_opts, key) - if val is not None: - opts[key] = val - priority = None - if build_opts.background: - # relative to koji.PRIO_DEFAULT - priority = 5 - task_id = session.winBuild(vm_name, scmurl, target, opts, priority=priority) - if not build_opts.quiet: - print("Created task: %d" % task_id) - print("Task info: %s/taskinfo?taskID=%s" % (options.weburl, task_id)) - if build_opts.wait or (build_opts.wait is None and not _running_in_bg()): - session.logout() - return watch_tasks(session, [task_id], quiet=build_opts.quiet, - poll_interval=options.poll_interval, topurl=options.topurl) - - -def handle_free_task(goptions, session, args): - "[admin] Free a task" - usage = "usage: %prog free-task [options] [ ...]" - parser = OptionParser(usage=get_usage_str(usage)) - (options, args) = parser.parse_args(args) - activate_session(session, goptions) - tlist = [] - for task_id in args: - try: - tlist.append(int(task_id)) - except ValueError: - parser.error("task_id must be an integer") - if not tlist: - parser.error("please specify at least one task_id") - for task_id in tlist: - session.freeTask(task_id) - - -def handle_cancel(goptions, session, args): - "[build] Cancel tasks and/or builds" - usage = "usage: %prog cancel [options] [ ...]" - parser = OptionParser(usage=get_usage_str(usage)) - parser.add_option("--justone", action="store_true", help="Do not cancel subtasks") - parser.add_option("--full", action="store_true", help="Full cancellation (admin only)") - parser.add_option("--force", action="store_true", help="Allow subtasks with --full") - (options, args) = parser.parse_args(args) - if len(args) == 0: - parser.error("You must specify at least one task id or build") - activate_session(session, goptions) - tlist = [] - blist = [] - for arg in args: - try: - tlist.append(int(arg)) - except ValueError: - try: - koji.parse_NVR(arg) - blist.append(arg) - except koji.GenericError: - parser.error("please specify only task ids (integer) or builds (n-v-r)") - - results = [] - with session.multicall(strict=False, batch=100) as m: - if tlist: - opts = {} - remote_fn = m.cancelTask - if options.justone: - opts['recurse'] = False - elif options.full: - remote_fn = m.cancelTaskFull - if options.force: - opts['strict'] = False - for task_id in tlist: - results.append(remote_fn(task_id, **opts)) - for build in blist: - results.append(m.cancelBuild(build)) - - err = False - for r in results: - if isinstance(r.result, dict): - warn(r.result['faultString']) - err = True - if err: - return 1 - - -def handle_set_task_priority(goptions, session, args): - "[admin] Set task priority" - usage = "usage: %prog set-task-priority [options] --priority= " \ - "[ ...]" - parser = OptionParser(usage=get_usage_str(usage)) - parser.add_option("--priority", type="int", help="New priority") - parser.add_option("--recurse", action="store_true", default=False, - help="Change priority of child tasks as well") - (options, args) = parser.parse_args(args) - if len(args) == 0: - parser.error("You must specify at least one task id") - - if options.priority is None: - parser.error("You must specify --priority") - try: - tasks = [int(a) for a in args] - except ValueError: - parser.error("Task numbers must be integers") - - activate_session(session, goptions) - - if not session.hasPerm('admin'): - logged_user = session.getLoggedInUser() - error("admin permission required (logged in as %s)" % logged_user['name']) - - for task_id in tasks: - try: - session.setTaskPriority(task_id, options.priority, options.recurse) - except koji.GenericError: - warn("Can't update task priority on closed task: %s" % task_id) - - -def handle_list_tasks(goptions, session, args): - "[info] Print the list of tasks" - usage = "usage: %prog list-tasks [options]" - parser = OptionParser(usage=get_usage_str(usage), option_class=TimeOption) - parser.add_option("--mine", action="store_true", help="Just print your tasks") - parser.add_option("--user", help="Only tasks for this user") - parser.add_option("--arch", help="Only tasks for this architecture") - parser.add_option("--method", help="Only tasks of this method") - parser.add_option("--channel", help="Only tasks in this channel") - parser.add_option("--host", help="Only tasks for this host") - parser.add_option("--quiet", action="store_true", default=goptions.quiet, - help="Do not display the column headers") - parser.add_option("--before", type="time", - help="List tasks completed before this time, " + TimeOption.get_help()) - parser.add_option("--after", type="time", - help="List tasks completed after this time (same format as for --before") - parser.add_option("--all", action="store_true", - help="List also finished tasks (valid only with --after)") - (options, args) = parser.parse_args(args) - if len(args) != 0: - parser.error("This command takes no arguments") - - if options.all and not options.after: - parser.error("--all must be used with --after") - - activate_session(session, goptions) - tasklist = _list_tasks(options, session) - if not tasklist: - print("(no tasks)") - return - if not options.quiet: - print_task_headers() - for t in tasklist: - if t.get('sub'): - # this subtask will appear under another task - continue - print_task_recurse(t) - - -def handle_set_pkg_arches(goptions, session, args): - "[admin] Set the list of extra arches for a package" - usage = "usage: %prog set-pkg-arches [options] [ ...]" - parser = OptionParser(usage=get_usage_str(usage)) - parser.add_option("--force", action='store_true', help="Force operation") - (options, args) = parser.parse_args(args) - if len(args) < 3: - parser.error("Please specify an archlist, a tag, and at least one package") - activate_session(session, goptions) - arches = koji.parse_arches(args[0]) - tag = args[1] - with session.multicall(strict=True) as m: - for package in args[2:]: - m.packageListSetArches(tag, package, arches, force=options.force) - - -def handle_set_pkg_owner(goptions, session, args): - "[admin] Set the owner for a package" - usage = "usage: %prog set-pkg-owner [options] [ ...]" - parser = OptionParser(usage=get_usage_str(usage)) - parser.add_option("--force", action='store_true', help="Force operation") - (options, args) = parser.parse_args(args) - if len(args) < 3: - parser.error("Please specify an owner, a tag, and at least one package") - activate_session(session, goptions) - owner = args[0] - tag = args[1] - with session.multicall(strict=True) as m: - for package in args[2:]: - m.packageListSetOwner(tag, package, owner, force=options.force) - - -def handle_set_pkg_owner_global(goptions, session, args): - "[admin] Set the owner for a package globally" - usage = "usage: %prog set-pkg-owner-global [options] [ ...]" - parser = OptionParser(usage=get_usage_str(usage)) - parser.add_option("--verbose", action='store_true', help="List changes") - parser.add_option("--test", action='store_true', help="Test mode") - parser.add_option("--old-user", "--from", action="store", - help="Only change ownership for packages belonging to this user") - (options, args) = parser.parse_args(args) - if options.old_user: - if len(args) < 1: - parser.error("Please specify an owner") - elif len(args) < 2: - parser.error("Please specify an owner and at least one package") - activate_session(session, goptions) - owner = args[0] - packages = args[1:] - user = session.getUser(owner) - if not user: - error("No such user: %s" % owner) - opts = {'with_dups': True} - old_user = None - if options.old_user: - old_user = session.getUser(options.old_user) - if not old_user: - error("No such user: %s" % options.old_user) - opts['userID'] = old_user['id'] - to_change = [] - for package in packages: - entries = session.listPackages(pkgID=package, **opts) - if not entries: - print("No data for package %s" % package) - continue - to_change.extend(entries) - if not packages and options.old_user: - entries = session.listPackages(**opts) - if not entries: - error("No data for user %s" % old_user['name']) - to_change.extend(entries) - for entry in to_change: - if user['id'] == entry['owner_id']: - if options.verbose: - print("Preserving owner=%s for package %s in tag %s" - % (user['name'], package, entry['tag_name'])) - else: - if options.test: - print("Would have changed owner for %s in tag %s: %s -> %s" - % (entry['package_name'], entry['tag_name'], entry['owner_name'], - user['name'])) - continue - if options.verbose: - print("Changing owner for %s in tag %s: %s -> %s" - % (entry['package_name'], entry['tag_name'], entry['owner_name'], - user['name'])) - session.packageListSetOwner(entry['tag_id'], entry['package_name'], user['id']) - - -def anon_handle_watch_task(goptions, session, args): - "[monitor] Track progress of particular tasks" - usage = "usage: %prog watch-task [options] [ ...]" - parser = OptionParser(usage=get_usage_str(usage)) - parser.add_option("--quiet", action="store_true", default=goptions.quiet, - help="Do not print the task information") - parser.add_option("--mine", action="store_true", help="Just watch your tasks") - parser.add_option("--user", help="Only tasks for this user") - parser.add_option("--arch", help="Only tasks for this architecture") - parser.add_option("--method", help="Only tasks of this method") - parser.add_option("--channel", help="Only tasks in this channel") - parser.add_option("--host", help="Only tasks for this host") - (options, args) = parser.parse_args(args) - selection = (options.mine or - options.user or - options.arch or - options.method or - options.channel or - options.host) - if args and selection: - parser.error("Selection options cannot be combined with a task list") - - if options.mine: - activate_session(session, goptions) - else: - ensure_connection(session, goptions) - if selection: - tasks = [task['id'] for task in _list_tasks(options, session)] - if not tasks: - print("(no tasks)") - return - else: - tasks = [] - for task in args: - try: - tasks.append(int(task)) - except ValueError: - parser.error("task id must be an integer") - if not tasks: - parser.error("at least one task id must be specified") - - return watch_tasks(session, tasks, quiet=options.quiet, - poll_interval=goptions.poll_interval, topurl=goptions.topurl) - - -def anon_handle_watch_logs(goptions, session, args): - "[monitor] Watch logs in realtime" - usage = "usage: %prog watch-logs [options] [ ...]" - parser = OptionParser(usage=get_usage_str(usage)) - parser.add_option("--log", help="Watch only a specific log") - parser.add_option("--mine", action="store_true", - help="Watch logs for all your tasks, task_id arguments are forbidden in " - "this case.") - parser.add_option("--follow", action="store_true", help="Follow spawned child tasks") - (options, args) = parser.parse_args(args) - - if options.mine: - activate_session(session, goptions) - if args: - parser.error("Selection options cannot be combined with a task list") - tasks = _list_tasks(options, session) - tasks = [t['id'] for t in tasks] - if not tasks: - print("You've no active tasks.") - return - else: - ensure_connection(session, goptions) - tasks = [] - for task in args: - try: - tasks.append(int(task)) - except ValueError: - parser.error("task id must be an integer") - if not tasks: - parser.error("at least one task id must be specified") - - watch_logs(session, tasks, options, goptions.poll_interval) - - -def handle_make_task(goptions, session, args): - "[admin] Create an arbitrary task" - usage = "usage: %prog make-task [options] [ ...]" - parser = OptionParser(usage=get_usage_str(usage)) - parser.add_option("--channel", help="set channel") - parser.add_option("--priority", help="set priority") - parser.add_option("--watch", action="store_true", help="watch the task") - parser.add_option("--arch", help="set arch") - (options, args) = parser.parse_args(args) - if len(args) < 1: - parser.error("Please specify task method at least") - - activate_session(session, goptions) - taskopts = {} - for key in ('channel', 'priority', 'arch'): - value = getattr(options, key, None) - if value is not None: - taskopts[key] = value - task_id = session.makeTask(method=args[0], - arglist=list(map(arg_filter, args[1:])), - **taskopts) - print("Created task id %d" % task_id) - if _running_in_bg() or not options.watch: - return - else: - session.logout() - return watch_tasks(session, [task_id], quiet=goptions.quiet, - poll_interval=goptions.poll_interval, topurl=goptions.topurl) - - -def handle_tag_build(opts, session, args): - "[bind] Apply a tag to one or more builds" - usage = "usage: %prog tag-build [options] [ ...]" - parser = OptionParser(usage=get_usage_str(usage)) - parser.add_option("--force", action="store_true", help="force operation") - parser.add_option("--wait", action="store_true", - help="Wait on task, even if running in the background") - parser.add_option("--nowait", action="store_false", dest="wait", help="Do not wait on task") - (options, args) = parser.parse_args(args) - if len(args) < 2: - parser.error( - "This command takes at least two arguments: a tag name/ID and one or more package " - "n-v-r's") - activate_session(session, opts) - tasks = [] - for pkg in args[1:]: - task_id = session.tagBuild(args[0], pkg, force=options.force) - # XXX - wait on task - tasks.append(task_id) - print("Created task %d" % task_id) - if options.wait or (options.wait is None and not _running_in_bg()): - session.logout() - return watch_tasks(session, tasks, quiet=opts.quiet, - poll_interval=opts.poll_interval, topurl=opts.topurl) - - -def handle_move_build(opts, session, args): - "[bind] 'Move' one or more builds between tags" - usage = "usage: %prog move-build [options] [ ...]" - parser = OptionParser(usage=get_usage_str(usage)) - parser.add_option("--force", action="store_true", help="force operation") - parser.add_option("--wait", action="store_true", - help="Wait on tasks, even if running in the background") - parser.add_option("--nowait", action="store_false", dest="wait", - help="Do not wait on tasks") - parser.add_option("--all", action="store_true", - help="move all instances of a package, 's are package names") - (options, args) = parser.parse_args(args) - if len(args) < 3: - if options.all: - parser.error( - "This command, with --all, takes at least three arguments: two tags and one or " - "more package names") - else: - parser.error( - "This command takes at least three arguments: two tags and one or more package " - "n-v-r's") - activate_session(session, opts) - tasks = [] - builds = [] - - if options.all: - for arg in args[2:]: - pkg = session.getPackage(arg) - if not pkg: - print("No such package: %s, skipping." % arg) - continue - tasklist = session.moveAllBuilds(args[0], args[1], arg, options.force) - tasks.extend(tasklist) - else: - for arg in args[2:]: - build = session.getBuild(arg) - if not build: - print("No such build: %s, skipping." % arg) - continue - if build not in builds: - builds.append(build) - - for build in builds: - task_id = session.moveBuild(args[0], args[1], build['id'], options.force) - tasks.append(task_id) - print("Created task %d, moving %s" % (task_id, koji.buildLabel(build))) - if options.wait or (options.wait is None and not _running_in_bg()): - session.logout() - return watch_tasks(session, tasks, quiet=opts.quiet, - poll_interval=opts.poll_interval, topurl=opts.topurl) - - -def handle_untag_build(goptions, session, args): - "[bind] Remove a tag from one or more builds" - usage = "usage: %prog untag-build [options] [ ...]" - parser = OptionParser(usage=get_usage_str(usage)) - parser.add_option("--all", action="store_true", - help="untag all versions of the package in this tag, pkg is package name") - parser.add_option("--non-latest", action="store_true", - help="untag all versions of the package in this tag except the latest, " - "pkg is package name") - parser.add_option("-n", "--test", action="store_true", help="test mode") - parser.add_option("-v", "--verbose", action="store_true", help="print details") - parser.add_option("--force", action="store_true", help="force operation") - (options, args) = parser.parse_args(args) - if options.non_latest and options.force: - if len(args) < 1: - parser.error("Please specify a tag") - elif len(args) < 2: - parser.error( - "This command takes at least two arguments: a tag name/ID and one or more package " - "n-v-r's or package names") - activate_session(session, goptions) - tag = session.getTag(args[0]) - if not tag: - parser.error("No such tag: %s" % args[0]) - if options.all: - result = [] - with session.multicall() as m: - result.extend([m.queryHistory(tag=args[0], package=pkg, active=True) - for pkg in args[1:]]) - builds = [] - for r in result: - builds.extend(r.result['tag_listing']) - elif options.non_latest: - if options.force and len(args) == 1: - tagged = session.queryHistory(tag=args[0], active=True)['tag_listing'] - tagged = sorted(tagged, key=lambda k: (k['create_event']), reverse=True) - else: - result = [] - with session.multicall() as m: - result.extend([m.queryHistory(tag=args[0], package=pkg, active=True) - for pkg in args[1:]]) - tagged = [] - for r in result: - tagged.extend(r.result['tag_listing']) - tagged = sorted(tagged, key=lambda k: (k['create_event']), reverse=True) - # listTagged orders entries latest first - seen_pkg = {} - builds = [] - for binfo in tagged: - if binfo['name'] not in seen_pkg: - # latest for this package - nvr = '%s-%s-%s' % (binfo['name'], binfo['version'], binfo['release']) - if options.verbose: - print("Leaving latest build for package %s: %s" % (binfo['name'], nvr)) - else: - builds.append(binfo) - seen_pkg[binfo['name']] = 1 - else: - # find all pkg's builds in tag - pkgs = set([koji.parse_NVR(nvr)['name'] for nvr in args[1:]]) - result = [] - with session.multicall() as m: - result.extend([m.queryHistory(tag=args[0], pkg=pkg, active=True) for pkg in pkgs]) - tagged = [] - for r in result: - tagged.append(r.result['tag_listing']) - # flatten - tagged = list(itertools.chain(*[t for t in tagged])) - idx = dict([('%s-%s-%s' % (b['name'], b['version'], b['release']), b) for b in tagged]) - - # check exact builds - builds = [] - for nvr in args[1:]: - binfo = idx.get(nvr) - if binfo: - builds.append(binfo) - else: - # not in tag, see if it even exists - binfo = session.getBuild(nvr) - if not binfo: - warn("No such build: %s" % nvr) - else: - warn("Build %s not in tag %s" % (nvr, tag['name'])) - if not options.force: - error() - builds.reverse() - with session.multicall(strict=True) as m: - for binfo in builds: - build_nvr = '%s-%s-%s' % (binfo['name'], binfo['version'], binfo['release']) - if options.test: - print("would have untagged %s" % build_nvr) - else: - if options.verbose: - print("untagging %s" % build_nvr) - m.untagBuild(tag['name'], build_nvr, force=options.force) - - -def handle_unblock_pkg(goptions, session, args): - "[admin] Unblock a package in the listing for tag" - usage = "usage: %prog unblock-pkg [options] [ ...]" - parser = OptionParser(usage=get_usage_str(usage)) - (options, args) = parser.parse_args(args) - if len(args) < 2: - parser.error("Please specify a tag and at least one package") - activate_session(session, goptions) - tag = args[0] - with session.multicall(strict=True) as m: - for package in args[1:]: - m.packageListUnblock(tag, package) - - -def anon_handle_download_build(options, session, args): - "[download] Download a built package" - usage = "usage: %prog download-build [options] " - parser = OptionParser(usage=get_usage_str(usage)) - parser.add_option("--arch", "-a", dest="arches", metavar="ARCH", action="append", default=[], - help="Only download packages for this arch (may be used multiple times)") - parser.add_option("--type", - help="Download archives of the given type, rather than rpms " - "(maven, win, image, remote-sources)") - parser.add_option("--latestfrom", dest="latestfrom", - help="Download the latest build from this tag") - parser.add_option("--debuginfo", action="store_true", help="Also download -debuginfo rpms") - parser.add_option("--task-id", action="store_true", help="Interperet id as a task id") - parser.add_option("--rpm", action="store_true", help="Download the given rpm") - parser.add_option("--key", help="Download rpms signed with the given key") - parser.add_option("--topurl", metavar="URL", default=options.topurl, - help="URL under which Koji files are accessible") - parser.add_option("--noprogress", action="store_true", help="Do not display progress meter") - parser.add_option("-q", "--quiet", action="store_true", - help="Suppress output", default=options.quiet) - (suboptions, args) = parser.parse_args(args) - if len(args) < 1: - parser.error("Please specify a package N-V-R or build ID") - elif len(args) > 1: - parser.error("Only a single package N-V-R or build ID may be specified") - - ensure_connection(session, options) - build = args[0] - - if build.isdigit(): - if suboptions.latestfrom: - parser.error("--latestfrom not compatible with build IDs, specify a package name.") - build = int(build) - if suboptions.task_id: - builds = session.listBuilds(taskID=build) - if not builds: - error("No associated builds for task %s" % build) - build = builds[0]['build_id'] - - if suboptions.latestfrom: - # We want the latest build, not a specific build - try: - builds = session.listTagged(suboptions.latestfrom, latest=True, package=build, - type=suboptions.type) - except koji.GenericError as data: - error("Error finding latest build: %s" % data) - if not builds: - error("%s has no builds of %s" % (suboptions.latestfrom, build)) - info = builds[0] - elif suboptions.rpm: - rpminfo = session.getRPM(build) - if rpminfo is None: - error("No such rpm: %s" % build) - info = session.getBuild(rpminfo['build_id']) - else: - # if we're given an rpm name without --rpm, download the containing build - try: - koji.parse_NVRA(build) - rpminfo = session.getRPM(build) - build = rpminfo['build_id'] - except Exception: - pass - info = session.getBuild(build) - - if info is None: - error("No such build: %s" % build) - - if not suboptions.topurl: - error("You must specify --topurl to download files") - - archives = [] - rpms = [] - if suboptions.type: - archives = session.listArchives(buildID=info['id'], type=suboptions.type) - if not archives: - error("No %s archives available for %s" % (suboptions.type, koji.buildLabel(info))) - else: - arches = suboptions.arches - if len(arches) == 0: - arches = None - if suboptions.rpm: - all_rpms = [rpminfo] - else: - all_rpms = session.listRPMs(buildID=info['id'], arches=arches) - if not all_rpms: - if arches: - error("No %s packages available for %s" % - (" or ".join(arches), koji.buildLabel(info))) - else: - error("No packages available for %s" % koji.buildLabel(info)) - for rpm in all_rpms: - if not suboptions.debuginfo and koji.is_debuginfo(rpm['name']): - continue - rpms.append(rpm) - - if suboptions.key: - with session.multicall() as m: - results = [m.queryRPMSigs(rpm_id=r['id'], sigkey=suboptions.key) for r in rpms] - rpm_keys = [x.result for x in results] - for rpm, rpm_key in list(zip(rpms, rpm_keys)): - if not rpm_key: - nvra = "%(nvr)s-%(arch)s.rpm" % rpm - warn("No such sigkey %s for rpm %s" % (suboptions.key, nvra)) - rpms.remove(rpm) - - # run the download - for rpm in rpms: - download_rpm(info, rpm, suboptions.topurl, sigkey=suboptions.key, - quiet=suboptions.quiet, noprogress=suboptions.noprogress) - for archive in archives: - download_archive(info, archive, suboptions.topurl, - quiet=suboptions.quiet, noprogress=suboptions.noprogress) - - -def anon_handle_download_logs(options, session, args): - "[download] Download logs for task" - - FAIL_LOG = "task_failed.log" - usage = "usage: %prog download-logs [options] [ ...]" - usage += "\n %prog download-logs [options] --nvr [ ...]" - usage += "\n" - usage += "\n" - usage += "Note this command only downloads task logs, not build logs." - usage += "\n" - parser = OptionParser(usage=get_usage_str(usage)) - parser.add_option("-r", "--recurse", action="store_true", - help="Process children of this task as well") - parser.add_option("--nvr", action="store_true", - help="Get the logs for the task associated with this build " - "Name-Version-Release.") - parser.add_option("-m", "--match", action="append", metavar="PATTERN", - help="Get only log filenames matching PATTERN (fnmatch). " - "May be used multiple times.") - parser.add_option("-c", "--continue", action="store_true", dest="cont", - help="Continue previous download") - parser.add_option("-d", "--dir", metavar="DIRECTORY", default='kojilogs', - help="Write logs to DIRECTORY") - (suboptions, args) = parser.parse_args(args) - - if len(args) < 1: - parser.error("Please specify at least one task id or n-v-r") - - def write_fail_log(task_log_dir, task_id): - """Gets output only from failed tasks""" - try: - result = session.getTaskResult(task_id) - # with current code, failed task results should always be faults, - # but that could change in the future - content = pprint.pformat(result) - except (six.moves.xmlrpc_client.Fault, koji.GenericError): - etype, e = sys.exc_info()[:2] - content = ''.join(traceback.format_exception_only(etype, e)) - full_filename = os.path.normpath(os.path.join(task_log_dir, FAIL_LOG)) - koji.ensuredir(os.path.dirname(full_filename)) - sys.stdout.write("Writing: %s\n" % full_filename) - with open(full_filename, 'wt') as fo: - fo.write(content) - - def download_log(task_log_dir, task_id, filename, blocksize=102400, volume=None): - # Create directories only if there is any log file to write to - # For each non-default volume create special sub-directory - if volume not in (None, 'DEFAULT'): - full_filename = os.path.normpath(os.path.join(task_log_dir, volume, filename)) - else: - full_filename = os.path.normpath(os.path.join(task_log_dir, filename)) - koji.ensuredir(os.path.dirname(full_filename)) - contents = 'IGNORE ME!' - if suboptions.cont and os.path.exists(full_filename): - sys.stdout.write("Continuing: %s\n" % full_filename) - fd = open(full_filename, 'ab') - offset = fd.tell() - else: - sys.stdout.write("Downloading: %s\n" % full_filename) - fd = open(full_filename, 'wb') - offset = 0 - try: - while contents: - contents = session.downloadTaskOutput(task_id, filename, offset=offset, - size=blocksize, volume=volume) - offset += len(contents) - if contents: - fd.write(contents) - finally: - fd.close() - - def save_logs(task_id, match, parent_dir='.', recurse=True): - assert task_id == int(task_id), "Task id must be number: %r" % task_id - task_info = session.getTaskInfo(task_id) - if task_info is None: - error("No such task: %d" % task_id) - files = list_task_output_all_volumes(session, task_id) - logs = [] # list of tuples (filename, volume) - for filename in files: - if not filename.endswith(".log"): - continue - if match and not koji.util.multi_fnmatch(filename, match): - continue - logs += [(filename, volume) for volume in files[filename]] - - task_log_dir = os.path.join(parent_dir, - "%s-%s" % (task_info["arch"], task_id)) - - count = 0 - state = koji.TASK_STATES[task_info['state']] - if state == 'FAILED': - if not match or koji.util.multi_fnmatch(FAIL_LOG, match): - write_fail_log(task_log_dir, task_id) - count += 1 - elif state not in ['CLOSED', 'CANCELED']: - warn("Task %s is %s\n" % (task_id, state)) - - for log_filename, log_volume in logs: - download_log(task_log_dir, task_id, log_filename, volume=log_volume) - count += 1 - - if count == 0 and not recurse: - warn("No logs found for task %i. Perhaps try --recurse?\n" % task_id) - - if recurse: - child_tasks = session.getTaskChildren(task_id) - for child_task in child_tasks: - save_logs(child_task['id'], match, task_log_dir, recurse) - - ensure_connection(session, options) - task_id = None - build_id = None - for arg in args: - if suboptions.nvr: - suboptions.recurse = True - binfo = session.getBuild(arg) - if binfo is None: - error("There is no build with n-v-r: %s" % arg) - if binfo.get('task_id'): - task_id = binfo['task_id'] - sys.stdout.write("Using task ID: %s\n" % task_id) - elif binfo.get('build_id'): - build_id = binfo['build_id'] - sys.stdout.write("Using build ID: %s\n" % build_id) - else: - try: - task_id = int(arg) - except ValueError: - error("Task id must be number: %r" % arg) - if task_id: - save_logs(task_id, suboptions.match, suboptions.dir, suboptions.recurse) - elif build_id: - logs = session.getBuildLogs(build_id) - match = suboptions.match - for log in logs: - url = os.path.join(options.topurl, log['path']) - filepath = os.path.join(os.getcwd(), '%s/%s/%s' % (suboptions.dir, - arg, log['name'])) - if not filepath.endswith(".log"): - continue - if match and not koji.util.multi_fnmatch(log['name'], match): - continue - download_file(url, filepath) - - -def anon_handle_download_task(options, session, args): - "[download] Download the output of a build task" - usage = "usage: %prog download-task " - parser = OptionParser(usage=get_usage_str(usage)) - parser.add_option("--arch", dest="arches", metavar="ARCH", action="append", default=[], - help="Only download packages for this arch (may be used multiple times)") - parser.add_option("--logs", dest="logs", action="store_true", default=False, - help="Also download build logs") - parser.add_option("--topurl", metavar="URL", default=options.topurl, - help="URL under which Koji files are accessible") - parser.add_option("--noprogress", action="store_true", help="Do not display progress meter") - parser.add_option("--wait", action="store_true", - help="Wait for running tasks to finish, even if running in the background") - parser.add_option("--nowait", action="store_false", dest="wait", - help="Do not wait for running tasks to finish") - parser.add_option("-q", "--quiet", action="store_true", - help="Suppress output", default=options.quiet) - - (suboptions, args) = parser.parse_args(args) - if len(args) == 0: - parser.error("Please specify a task ID") - elif len(args) > 1: - parser.error("Only one task ID may be specified") - - base_task_id = int(args.pop()) - if len(suboptions.arches) > 0: - suboptions.arches = ",".join(suboptions.arches).split(",") - - ensure_connection(session, options) - - # get downloadable tasks - - base_task = session.getTaskInfo(base_task_id) - if not base_task: - error('No such task: %d' % base_task_id) - - if (suboptions.wait or (suboptions.wait is None and not _running_in_bg())) and \ - base_task['state'] not in ( - koji.TASK_STATES['CLOSED'], - koji.TASK_STATES['CANCELED'], - koji.TASK_STATES['FAILED']): - watch_tasks(session, [base_task_id], quiet=suboptions.quiet, - poll_interval=options.poll_interval, topurl=options.topurl) - - def check_downloadable(task): - return task["method"] == "buildArch" - - downloadable_tasks = [] - - if check_downloadable(base_task): - downloadable_tasks.append(base_task) - else: - subtasks = session.getTaskChildren(base_task_id) - downloadable_tasks.extend(list(filter(check_downloadable, subtasks))) - - # get files for download - downloads = [] - - for task in downloadable_tasks: - files = list_task_output_all_volumes(session, task["id"]) - for filename in files: - if filename.endswith(".rpm"): - for volume in files[filename]: - filearch = filename.split(".")[-2] - if len(suboptions.arches) == 0 or filearch in suboptions.arches: - downloads.append((task, filename, volume, filename)) - elif filename.endswith(".log") and suboptions.logs: - for volume in files[filename]: - # rename logs, they would conflict - new_filename = "%s.%s.log" % (filename.rstrip(".log"), task["arch"]) - downloads.append((task, filename, volume, new_filename)) - - if len(downloads) == 0: - error("No files for download found.") - - required_tasks = {} - for (task, nop, nop, nop) in downloads: - if task["id"] not in required_tasks: - required_tasks[task["id"]] = task - - for task_id in required_tasks: - if required_tasks[task_id]["state"] != koji.TASK_STATES.get("CLOSED"): - if task_id == base_task_id: - error("Task %d has not finished yet." % task_id) - else: - error("Child task %d has not finished yet." % task_id) - - # perform the download - number = 0 - pathinfo = koji.PathInfo(topdir=suboptions.topurl) - for (task, filename, volume, new_filename) in downloads: - number += 1 - if volume not in (None, 'DEFAULT'): - koji.ensuredir(volume) - new_filename = os.path.join(volume, new_filename) - if '..' in filename: - error('Invalid file name: %s' % filename) - url = '%s/%s/%s' % (pathinfo.work(volume), pathinfo.taskrelpath(task["id"]), filename) - download_file(url, new_filename, quiet=suboptions.quiet, noprogress=suboptions.noprogress, - size=len(downloads), num=number) - - -def anon_handle_wait_repo(options, session, args): - "[monitor] Wait for a repo to be regenerated" - usage = "usage: %prog wait-repo [options] " - parser = OptionParser(usage=get_usage_str(usage)) - parser.add_option("--build", metavar="NVR", dest="builds", action="append", default=[], - help="Check that the given build is in the newly-generated repo " - "(may be used multiple times)") - parser.add_option("--target", action="store_true", - help="Interpret the argument as a build target name") - parser.add_option("--timeout", type="int", default=120, - help="Amount of time to wait (in minutes) before giving up " - "(default: 120)") - parser.add_option("--quiet", action="store_true", default=options.quiet, - help="Suppress output, success or failure will be indicated by the return " - "value only") - (suboptions, args) = parser.parse_args(args) - - start = time.time() - - builds = [koji.parse_NVR(build) for build in suboptions.builds] - if len(args) < 1: - parser.error("Please specify a tag name") - elif len(args) > 1: - parser.error("Only one tag may be specified") - - tag = args[0] - - ensure_connection(session, options) - if suboptions.target: - target_info = session.getBuildTarget(tag) - if not target_info: - parser.error("No such build target: %s" % tag) - tag = target_info['build_tag_name'] - tag_id = target_info['build_tag'] - else: - tag_info = session.getTag(tag) - if not tag_info: - parser.error("No such tag: %s" % tag) - targets = session.getBuildTargets(buildTagID=tag_info['id']) - if not targets: - warn("%(name)s is not a build tag for any target" % tag_info) - targets = session.getBuildTargets(destTagID=tag_info['id']) - if targets: - maybe = {}.fromkeys([t['build_tag_name'] for t in targets]) - maybe = sorted(maybe.keys()) - warn("Suggested tags: %s" % ', '.join(maybe)) - error() - tag_id = tag_info['id'] - - for nvr in builds: - data = session.getLatestBuilds(tag_id, package=nvr["name"]) - if len(data) == 0: - warn("Package %s is not in tag %s" % (nvr["name"], tag)) - else: - present_nvr = [x["nvr"] for x in data][0] - expected_nvr = '%(name)s-%(version)s-%(release)s' % nvr - if present_nvr != expected_nvr: - warn("nvr %s is not current in tag %s\n latest build in %s is %s" % - (expected_nvr, tag, tag, present_nvr)) - - last_repo = None - repo = session.getRepo(tag_id) - - while True: - if builds and repo and repo != last_repo: - if koji.util.checkForBuilds(session, tag_id, builds, repo['create_event'], - latest=True): - if not suboptions.quiet: - print("Successfully waited %s for %s to appear in the %s repo" % - (koji.util.duration(start), koji.util.printList(suboptions.builds), tag)) - return - - if (time.time() - start) >= (suboptions.timeout * 60.0): - if not suboptions.quiet: - if builds: - error("Unsuccessfully waited %s for %s to appear in the %s repo" % - (koji.util.duration(start), koji.util.printList(suboptions.builds), tag)) - else: - error("Unsuccessfully waited %s for a new %s repo" % - (koji.util.duration(start), tag)) - error() - - time.sleep(options.poll_interval) - last_repo = repo - repo = session.getRepo(tag_id) - - if not builds: - if repo != last_repo: - if not suboptions.quiet: - print("Successfully waited %s for a new %s repo" % - (koji.util.duration(start), tag)) - return - - -def handle_regen_repo(options, session, args): - "[admin] Force a repo to be regenerated" - usage = "usage: %prog regen-repo [options] " - parser = OptionParser(usage=get_usage_str(usage)) - parser.add_option("--target", action="store_true", - help="Interpret the argument as a build target name") - parser.add_option("--wait", action="store_true", - help="Wait on for regen to finish, even if running in the background") - parser.add_option("--nowait", action="store_false", dest="wait", - help="Don't wait on for regen to finish") - parser.add_option("--debuginfo", action="store_true", help="Include debuginfo rpms in repo") - parser.add_option("--source", "--src", action="store_true", - help="Include source rpms in each of repos") - parser.add_option("--separate-source", "--separate-src", action="store_true", - help="Include source rpms in separate src repo") - (suboptions, args) = parser.parse_args(args) - if len(args) == 0: - parser.error("A tag name must be specified") - elif len(args) > 1: - if suboptions.target: - parser.error("Only a single target may be specified") - else: - parser.error("Only a single tag name may be specified") - activate_session(session, options) - tag = args[0] - repo_opts = {} - if suboptions.target: - info = session.getBuildTarget(tag) - if not info: - parser.error("No such build target: %s" % tag) - tag = info['build_tag_name'] - info = session.getTag(tag, strict=True) - else: - info = session.getTag(tag) - if not info: - parser.error("No such tag: %s" % tag) - tag = info['name'] - targets = session.getBuildTargets(buildTagID=info['id']) - if not targets: - warn("%s is not a build tag" % tag) - if not info['arches']: - warn("Tag %s has an empty arch list" % info['name']) - if suboptions.debuginfo: - repo_opts['debuginfo'] = True - if suboptions.source: - repo_opts['src'] = True - if suboptions.separate_source: - repo_opts['separate_src'] = True - task_id = session.newRepo(tag, **repo_opts) - print("Regenerating repo for tag: %s" % tag) - print("Created task: %d" % task_id) - print("Task info: %s/taskinfo?taskID=%s" % (options.weburl, task_id)) - if suboptions.wait or (suboptions.wait is None and not _running_in_bg()): - session.logout() - return watch_tasks(session, [task_id], quiet=options.quiet, - poll_interval=options.poll_interval, topurl=options.topurl) - - -def handle_dist_repo(options, session, args): - """Create a yum repo with distribution options""" - usage = "usage: %prog dist-repo [options] [ ...]\n\n" \ - "In normal mode, dist-repo behaves like any other koji task.\n" \ - "Sometimes you want to limit running distRepo tasks per tag to only\n" \ - "one. For such behaviour admin (with 'tag' permission) needs to\n" \ - "modify given tag's extra field 'distrepo.cancel_others' to True'\n" \ - "via 'koji edit-tag -x distrepo.cancel_others=True'\n" - usage += "\n(Specify the --help option for a list of other options)" - parser = OptionParser(usage=usage) - parser.add_option('--allow-missing-signatures', action='store_true', - default=False, - help='For RPMs not signed with a desired key, fall back to the primary copy') - parser.add_option("-a", "--arch", action='append', default=[], - help="Indicate an architecture to consider. The default is all " - "architectures associated with the given tag. This option may " - "be specified multiple times.") - parser.add_option("--with-src", action='store_true', help='Also generate a src repo') - parser.add_option("--split-debuginfo", action='store_true', default=False, - help='Split debuginfo info a separate repo for each arch') - parser.add_option('--comps', help='Include a comps file in the repodata') - parser.add_option('--delta-rpms', metavar='REPO', default=[], action='append', - help='Create delta rpms. REPO can be the id of another dist repo ' - 'or the name of a tag that has a dist repo. May be specified ' - 'multiple times.') - parser.add_option('--event', type='int', help='Use tag content at event') - parser.add_option("--volume", help="Generate repo on given volume") - parser.add_option('--non-latest', dest='latest', default=True, - action='store_false', help='Include older builds, not just the latest') - parser.add_option('--multilib', default=None, metavar="CONFIG", - help='Include multilib packages in the repository using the given ' - 'config file') - parser.add_option("--noinherit", action='store_true', default=False, - help='Do not consider tag inheritance') - parser.add_option("--wait", action="store_true", - help="Wait for the task to complete, even if running in the background") - parser.add_option("--nowait", action="store_false", dest="wait", - help="Do not wait for the task to complete") - parser.add_option('--skip-missing-signatures', action='store_true', default=False, - help='Skip RPMs not signed with the desired key(s)') - parser.add_option('--zck', action='store_true', default=False, - help='Generate zchunk files as well as the standard repodata') - parser.add_option('--zck-dict-dir', action='store', default=None, - help='Directory containing compression dictionaries for use by zchunk ' - '(on builder)') - parser.add_option("--write-signed-rpms", action='store_true', default=False, - help='Write a signed rpms for given tag') - task_opts, args = parser.parse_args(args) - if len(args) < 1: - parser.error('You must provide a tag to generate the repo from') - if len(args) < 2 and not task_opts.allow_missing_signatures: - parser.error('Please specify one or more GPG key IDs (or --allow-missing-signatures)') - if task_opts.allow_missing_signatures and task_opts.skip_missing_signatures: - parser.error('allow_missing_signatures and skip_missing_signatures are mutually exclusive') - activate_session(session, options) - stuffdir = unique_path('cli-dist-repo') - if task_opts.comps: - if not os.path.exists(task_opts.comps): - parser.error('could not find %s' % task_opts.comps) - session.uploadWrapper(task_opts.comps, stuffdir, - callback=_progress_callback) - print('') - task_opts.comps = os.path.join(stuffdir, - os.path.basename(task_opts.comps)) - old_repos = [] - if len(task_opts.delta_rpms) > 0: - for repo in task_opts.delta_rpms: - if repo.isdigit(): - rinfo = session.repoInfo(int(repo), strict=True) - else: - # get dist repo for tag - rinfo = session.getRepo(repo, dist=True) - if not rinfo: - # maybe there is an expired one - rinfo = session.getRepo(repo, - state=koji.REPO_STATES['EXPIRED'], dist=True) - if not rinfo: - parser.error("Can't find repo for tag: %s" % repo) - old_repos.append(rinfo['id']) - tag = args[0] - keys = args[1:] - taginfo = session.getTag(tag) - if not taginfo: - parser.error('No such tag: %s' % tag) - allowed_arches = taginfo['arches'] or '' - if not allowed_arches: - for tag_inh in session.getFullInheritance(tag): - allowed_arches = session.getTag(tag_inh['parent_id'])['arches'] or '' - if allowed_arches: - break - if len(task_opts.arch) == 0: - task_opts.arch = allowed_arches.split() - if not task_opts.arch: - parser.error('No arches given and no arches associated with tag') - else: - for a in task_opts.arch: - if not allowed_arches: - warn('Tag %s has an empty arch list' % taginfo['name']) - elif a not in allowed_arches: - warn('%s is not in the list of tag arches' % a) - if task_opts.multilib: - if not os.path.exists(task_opts.multilib): - parser.error('could not find %s' % task_opts.multilib) - if 'x86_64' in task_opts.arch and 'i686' not in task_opts.arch: - parser.error('The multilib arch (i686) must be included') - if 's390x' in task_opts.arch and 's390' not in task_opts.arch: - parser.error('The multilib arch (s390) must be included') - if 'ppc64' in task_opts.arch and 'ppc' not in task_opts.arch: - parser.error('The multilib arch (ppc) must be included') - session.uploadWrapper(task_opts.multilib, stuffdir, - callback=_progress_callback) - task_opts.multilib = os.path.join(stuffdir, - os.path.basename(task_opts.multilib)) - print('') - if 'noarch' in task_opts.arch: - task_opts.arch.remove('noarch') - if task_opts.with_src and 'src' not in task_opts.arch: - task_opts.arch.append('src') - if not task_opts.arch: - parser.error('No arches left.') - - opts = { - 'arch': task_opts.arch, - 'comps': task_opts.comps, - 'delta': old_repos, - 'event': task_opts.event, - 'volume': task_opts.volume, - 'inherit': not task_opts.noinherit, - 'latest': task_opts.latest, - 'multilib': task_opts.multilib, - 'split_debuginfo': task_opts.split_debuginfo, - 'skip_missing_signatures': task_opts.skip_missing_signatures, - 'allow_missing_signatures': task_opts.allow_missing_signatures, - 'zck': task_opts.zck, - 'zck_dict_dir': task_opts.zck_dict_dir, - 'write_signed_rpms': task_opts.write_signed_rpms, - } - task_id = session.distRepo(tag, keys, **opts) - print("Creating dist repo for tag " + tag) - if task_opts.wait or (task_opts.wait is None and not _running_in_bg()): - session.logout() - return watch_tasks(session, [task_id], quiet=options.quiet, - poll_interval=options.poll_interval, topurl=options.topurl) - - -_search_types = ('package', 'build', 'tag', 'target', 'user', 'host', 'rpm', - 'maven', 'win') - - -def anon_handle_search(goptions, session, args): - "[search] Search the system" - usage = "usage: %prog search [options] " - usage += '\nAvailable search types: %s' % ', '.join(_search_types) - parser = OptionParser(usage=get_usage_str(usage)) - parser.add_option("-r", "--regex", action="store_true", help="treat pattern as regex") - parser.add_option("--exact", action="store_true", help="exact matches only") - (options, args) = parser.parse_args(args) - if len(args) < 1: - parser.error("Please specify search type") - if len(args) < 2: - parser.error("Please specify search pattern") - type = args[0] - if type not in _search_types: - parser.error("No such search type: %s" % type) - pattern = args[1] - matchType = 'glob' - if options.regex: - matchType = 'regexp' - elif options.exact: - matchType = 'exact' - ensure_connection(session, goptions) - data = session.search(pattern, type, matchType) - for row in data: - print(row['name']) - - -def handle_moshimoshi(options, session, args): - "[misc] Introduce yourself" - usage = "usage: %prog moshimoshi [options]" - parser = OptionParser(usage=get_usage_str(usage)) - (opts, args) = parser.parse_args(args) - if len(args) != 0: - parser.error("This command takes no arguments") - activate_session(session, options) - u = session.getLoggedInUser() - if not u: - print("Not authenticated") - u = {'name': 'anonymous user'} - print("%s, %s!" % (_printable_unicode(random.choice(greetings)), u["name"])) - print("") - print("You are using the hub at %s" % session.baseurl) - authtype = u.get('authtype', getattr(session, 'authtype', None)) - if authtype == koji.AUTHTYPE_NORMAL: - print("Authenticated via password") - elif authtype == koji.AUTHTYPE_GSSAPI: - print("Authenticated via GSSAPI") - elif authtype == koji.AUTHTYPE_KERB: - print("Authenticated via Kerberos principal %s" % session.krb_principal) - elif authtype == koji.AUTHTYPE_SSL: - print("Authenticated via client certificate %s" % options.cert) - - -def anon_handle_list_notifications(goptions, session, args): - "[monitor] List user's notifications and blocks" - usage = "usage: %prog list-notifications [options]" - parser = OptionParser(usage=get_usage_str(usage)) - parser.add_option("--mine", action="store_true", help="Just print your notifications") - parser.add_option("--user", help="Only notifications for this user") - (options, args) = parser.parse_args(args) - - if len(args) != 0: - parser.error("This command takes no arguments") - if not options.mine and not options.user: - parser.error("Use --user or --mine.") - - if options.user: - ensure_connection(session, goptions) - user = session.getUser(options.user) - if not user: - error("No such user: %s" % options.user) - user_id = user['id'] - else: - activate_session(session, goptions) - user_id = None - - mask = "%(id)6s %(tag)-25s %(package)-25s %(email)-20s %(success)-12s" - headers = {'id': 'ID', - 'tag': 'Tag', - 'package': 'Package', - 'email': 'E-mail', - 'success': 'Success-only'} - head = mask % headers - notifications = session.getBuildNotifications(user_id) - if notifications: - print('Notifications') - print(head) - print('-' * len(head)) - for notification in notifications: - if notification['tag_id']: - notification['tag'] = session.getTag(notification['tag_id'])['name'] - else: - notification['tag'] = '*' - if notification['package_id']: - notification['package'] = session.getPackage(notification['package_id'])['name'] - else: - notification['package'] = '*' - notification['success'] = ['no', 'yes'][notification['success_only']] - print(mask % notification) - else: - print('No notifications') - - print('') - - mask = "%(id)6s %(tag)-25s %(package)-25s" - head = mask % headers - blocks = session.getBuildNotificationBlocks(user_id) - if blocks: - print('Notification blocks') - print(head) - print('-' * len(head)) - for notification in blocks: - if notification['tag_id']: - notification['tag'] = session.getTag(notification['tag_id'])['name'] - else: - notification['tag'] = '*' - if notification['package_id']: - notification['package'] = session.getPackage(notification['package_id'])['name'] - else: - notification['package'] = '*' - print(mask % notification) - else: - print('No notification blocks') - - -def handle_add_notification(goptions, session, args): - "[monitor] Add user's notification" - usage = "usage: %prog add-notification [options]" - parser = OptionParser(usage=get_usage_str(usage)) - parser.add_option("--user", help="Add notifications for this user (admin-only)") - parser.add_option("--package", help="Add notifications for this package") - parser.add_option("--tag", help="Add notifications for this tag") - parser.add_option("--success-only", action="store_true", default=False, help="") - (options, args) = parser.parse_args(args) - - if len(args) != 0: - parser.error("This command takes no arguments") - - if not options.package and not options.tag: - parser.error("Command need at least one from --tag or --package options.") - - activate_session(session, goptions) - - if options.user and not session.hasPerm('admin'): - parser.error("--user requires admin permission") - - if options.user: - user_id = session.getUser(options.user)['id'] - else: - user_id = session.getLoggedInUser()['id'] - - if options.package: - package_id = session.getPackageID(options.package) - if package_id is None: - parser.error("No such package: %s" % options.package) - else: - package_id = None - - if options.tag: - try: - tag_id = session.getTagID(options.tag, strict=True) - except koji.GenericError: - parser.error("No such tag: %s" % options.tag) - else: - tag_id = None - - session.createNotification(user_id, package_id, tag_id, options.success_only) - - -def handle_remove_notification(goptions, session, args): - "[monitor] Remove user's notifications" - usage = "usage: %prog remove-notification [options] [ ...]" - parser = OptionParser(usage=get_usage_str(usage)) - (options, args) = parser.parse_args(args) - - activate_session(session, goptions) - - if len(args) < 1: - parser.error("At least one notification id has to be specified") - - try: - n_ids = [int(x) for x in args] - except ValueError: - parser.error("All notification ids has to be integers") - - for n_id in n_ids: - session.deleteNotification(n_id) - if not goptions.quiet: - print("Notification %d successfully removed." % n_id) - - -def handle_edit_notification(goptions, session, args): - "[monitor] Edit user's notification" - usage = "usage: %prog edit-notification [options] " - parser = OptionParser(usage=get_usage_str(usage)) - parser.add_option("--package", help="Notifications for this package, '*' for all") - parser.add_option("--tag", help="Notifications for this tag, '*' for all") - parser.add_option("--success-only", action="store_true", default=None, - dest='success_only', help="Notify only on successful events") - parser.add_option("--no-success-only", action="store_false", - default=None, dest='success_only', help="Notify on all events") - (options, args) = parser.parse_args(args) - - if len(args) != 1: - parser.error("Only argument is notification ID") - - try: - n_id = int(args[0]) - except ValueError: - parser.error("Notification ID has to be numeric") - - if not options.package and not options.tag and options.success_only is None: - parser.error("Command need at least one option") - - activate_session(session, goptions) - - old = session.getBuildNotification(n_id) - - if options.package == '*': - package_id = None - elif options.package: - package_id = session.getPackageID(options.package) - if package_id is None: - parser.error("No such package: %s" % options.package) - else: - package_id = old['package_id'] - - if options.tag == '*': - tag_id = None - elif options.tag: - try: - tag_id = session.getTagID(options.tag, strict=True) - except koji.GenericError: - parser.error("No such tag: %s" % options.tag) - else: - tag_id = old['tag_id'] - - if options.success_only is not None: - success_only = options.success_only - else: - success_only = old['success_only'] - - session.updateNotification(n_id, package_id, tag_id, success_only) - - -def handle_block_notification(goptions, session, args): - "[monitor] Block user's notifications" - usage = "usage: %prog block-notification [options]" - parser = OptionParser(usage=get_usage_str(usage)) - parser.add_option("--user", help="Block notifications for this user (admin-only)") - parser.add_option("--package", help="Block notifications for this package") - parser.add_option("--tag", help="Block notifications for this tag") - parser.add_option("--all", action="store_true", help="Block all notification for this user") - (options, args) = parser.parse_args(args) - - if len(args) != 0: - parser.error("This command takes no arguments") - - if not options.package and not options.tag and not options.all: - parser.error("One of --tag, --package or --all must be specified.") - - activate_session(session, goptions) - - if options.user and not session.hasPerm('admin'): - parser.error("--user requires admin permission") - - if options.user: - user_id = session.getUser(options.user, strict=True)['id'] - else: - logged_in_user = session.getLoggedInUser() - if logged_in_user: - user_id = logged_in_user['id'] - else: - parser.error("Please login with authentication or specify --user") - - if options.package: - package_id = session.getPackageID(options.package) - if package_id is None: - parser.error("No such package: %s" % options.package) - else: - package_id = None - - if options.tag: - try: - tag_id = session.getTagID(options.tag, strict=True) - except koji.GenericError: - parser.error("No such tag: %s" % options.tag) - else: - tag_id = None - - for block in session.getBuildNotificationBlocks(user_id): - if block['package_id'] == package_id and block['tag_id'] == tag_id: - parser.error('Notification already exists.') - - session.createNotificationBlock(user_id, package_id, tag_id) - - -def handle_unblock_notification(goptions, session, args): - "[monitor] Unblock user's notification" - usage = "usage: %prog unblock-notification [options] [ ...]" - parser = OptionParser(usage=get_usage_str(usage)) - (options, args) = parser.parse_args(args) - - activate_session(session, goptions) - - if len(args) < 1: - parser.error("At least one notification block id has to be specified") - - try: - n_ids = [int(x) for x in args] - except ValueError: - parser.error("All notification block ids has to be integers") - - for n_id in n_ids: - session.deleteNotificationBlock(n_id) - if not goptions.quiet: - print("Notification block %d successfully removed." % n_id) - - -def handle_version(goptions, session, args): - """Report client and hub versions""" - ensure_connection(session, goptions) - print('Client: %s' % koji.__version__) - try: - version = session.getKojiVersion() - print("Hub: %s" % version) - except koji.GenericError: - print("Hub: Can't determine (older than 1.23)") - - -def anon_handle_userinfo(goptions, session, args): - """[admin] Show information about a user""" - usage = "usage: %prog userinfo [options] [ ...]" - parser = OptionParser(usage=get_usage_str(usage)) - (options, args) = parser.parse_args(args) - if len(args) < 1: - parser.error("You must specify at least one username") - - ensure_connection(session, goptions) - - with session.multicall() as m: - userinfos = [m.getUser(user) for user in args] - user_infos = [] - for username, userinfo in zip(args, userinfos): - if userinfo.result is None: - warn("No such user: %s\n" % username) - continue - user_infos.append(userinfo.result) - user_infos = list(filter(None, user_infos)) - - calls = [] - with session.multicall() as m: - for user in user_infos: - results = [] - if not user: - warn("No such user: %s\n" % user) - continue - results.append(m.getUserPerms(user['id'])) - results.append(m.listPackages(userID=user['id'], with_dups=True, - queryOpts={'countOnly': True})) - results.append(m.listTasks(opts={'owner': user['id'], 'parent': None}, - queryOpts={'countOnly': True})) - results.append(m.listBuilds(userID=user['id'], queryOpts={'countOnly': True})) - calls.append(results) - - for userinfo, (perms, pkgs, tasks, builds) in zip(user_infos, calls): - print("User name: %s" % userinfo['name']) - print("User ID: %d" % userinfo['id']) - if 'krb_principals' in userinfo: - print("krb principals:") - for krb in userinfo['krb_principals']: - print(" %s" % krb) - if perms.result: - print("Permissions:") - for perm in perms.result: - print(" %s" % perm) - print("Status: %s" % koji.USER_STATUS[userinfo['status']]) - print("Usertype: %s" % koji.USERTYPES[userinfo['usertype']]) - print("Number of packages: %d" % pkgs.result) - print("Number of tasks: %d" % tasks.result) - print("Number of builds: %d" % builds.result) - print('') From 74dedfcb70eec33c8b5c7e02dd6649f7527f4d6f Mon Sep 17 00:00:00 2001 From: Tester Date: Jun 13 2022 14:56:57 +0000 Subject: [PATCH 2/6] fix splitting script --- diff --git a/cli/koji_cli/split.py b/cli/koji_cli/split.py new file mode 100755 index 0000000..50b44f8 --- /dev/null +++ b/cli/koji_cli/split.py @@ -0,0 +1,107 @@ +#!/usr/bin/python3 +import os +import shutil + +HEAD = """from __future__ import absolute_import, division + +import ast +import fnmatch +import itertools +import hashlib +import json +import logging +import os +import pprint +import random +import re +import stat +import sys +import textwrap +import time +import traceback +from collections import OrderedDict, defaultdict +from datetime import datetime +from dateutil.tz import tzutc +from optparse import SUPPRESS_HELP, OptionParser + +import six +import six.moves.xmlrpc_client +from six.moves import filter, map, range, zip + +import koji +from koji.util import base64encode, md5_constructor, to_list + +from koji_cli.lib import ( + TimeOption, + DatetimeJSONEncoder, + _list_tasks, + _progress_callback, + _running_in_bg, + activate_session, + arg_filter, + download_archive, + download_file, + download_rpm, + ensure_connection, + error, + format_inheritance_flags, + get_usage_str, + greetings, + linked_upload, + list_task_output_all_volumes, + print_task_headers, + print_task_recurse, + unique_path, + warn, + watch_logs, + watch_tasks, + truncate_string +) +""" + +try: + shutil.rmtree('commands') +except: + pass +os.mkdir('commands') + +init = open('commands/__init__.py', 'wt') +out = open('xyz.py', 'wt') +out_cmd = None + +state = 'out' +for line in open('c.py'): + if state == 'out': + if line.startswith('def '): + fname = line[4:].split('(')[0] + if fname.startswith('handle_') or fname.startswith('anon_handle_'): + if fname.startswith('handle_'): + fn = f'cmd_{fname[7:]}' + else: + fn = f'cmd_{fname[12:]}' + out_cmd = open(os.path.join('commands', "%s.py" % fn), 'wt') + out_cmd.write(HEAD) + out_cmd.write(line) + init.write('from .%s import %s\n' % (fn, fname)) + state = 'in' + else: + out.write(line) + else: + out.write(line) + elif state == 'in': + if line.startswith('def '): + fname = line[4:].split('(')[0] + if fname.startswith('handle_') or fname.startswith('anon_handle_'): + if fname.startswith('handle_'): + fn = f'cmd_{fname[7:]}' + else: + fn = f'cmd_{fname[12:]}' + out_cmd = open(os.path.join('commands', "%s.py" % fn), 'wt') + out_cmd.write(HEAD) + out_cmd.write(line) + init.write('from .%s import %s\n' % (fn, fname)) + state = 'in' + else: + out_cmd.write(line) + else: + out_cmd.write(line) From 40bebfea2b6ef042f9a9b9c322db840967a289d2 Mon Sep 17 00:00:00 2001 From: Tester Date: Jun 13 2022 14:57:05 +0000 Subject: [PATCH 3/6] Split CLI commands to separate files Related: https://pagure.io/koji/issue/2116 --- diff --git a/cli/koji_cli/commands/__init__.py b/cli/koji_cli/commands/__init__.py new file mode 100644 index 0000000..02c95f5 --- /dev/null +++ b/cli/koji_cli/commands/__init__.py @@ -0,0 +1,19 @@ +import importlib +import inspect +import pkgutil +import six + +__all__ = [] + +for loader, name, is_pkg in pkgutil.walk_packages(__path__): + if six.PY2: + module = loader.find_module(name).load_module(name) + else: + spec = loader.find_spec(name) + module = importlib.util.module_from_spec(spec) + spec.loader.exec_module(module) + + for name, entry in inspect.getmembers(module): + if name.startswith('handle_') or name.startswith('anon_handle_'): + globals()[name] = entry + __all__.append(name) diff --git a/cli/koji_cli/commands/cmd_add_channel.py b/cli/koji_cli/commands/cmd_add_channel.py new file mode 100644 index 0000000..a954c8d --- /dev/null +++ b/cli/koji_cli/commands/cmd_add_channel.py @@ -0,0 +1,39 @@ +from __future__ import absolute_import, division + +from optparse import OptionParser + + +import koji + +from koji_cli.lib import ( + activate_session, + error, + get_usage_str +) + + +def handle_add_channel(goptions, session, args): + "[admin] Add a channel" + usage = "usage: %prog add-channel [options] " + parser = OptionParser(usage=get_usage_str(usage)) + parser.add_option("--description", help="Description of channel") + (options, args) = parser.parse_args(args) + if len(args) != 1: + parser.error("Please specify one channel name") + activate_session(session, goptions) + channel_name = args[0] + try: + channel_id = session.addChannel(channel_name, description=options.description) + except koji.GenericError as ex: + msg = str(ex) + if 'channel %s already exists' % channel_name in msg: + error("channel %s already exists" % channel_name) + elif 'Invalid method:' in msg: + version = session.getKojiVersion() + error("addChannel is available on hub from Koji 1.26 version, your version is %s" % + version) + else: + error(msg) + print("%s added: id %d" % (args[0], channel_id)) + + diff --git a/cli/koji_cli/commands/cmd_add_external_repo.py b/cli/koji_cli/commands/cmd_add_external_repo.py new file mode 100644 index 0000000..78d7c7e --- /dev/null +++ b/cli/koji_cli/commands/cmd_add_external_repo.py @@ -0,0 +1,60 @@ +from __future__ import absolute_import, division + +from optparse import OptionParser + + +import koji + +from koji_cli.lib import ( + activate_session, + get_usage_str +) + + +def handle_add_external_repo(goptions, session, args): + "[admin] Create an external repo and/or add one to a tag" + usage = "usage: %prog add-external-repo [options] []" + parser = OptionParser(usage=get_usage_str(usage)) + parser.add_option("-t", "--tag", action="append", metavar="TAG", + help="Also add repo to tag. Use tag::N to set priority") + parser.add_option("-p", "--priority", type='int', + help="Set priority (when adding to tag)") + parser.add_option("-m", "--mode", help="Set merge mode") + parser.add_option("-a", "--arches", metavar="ARCH1,ARCH2, ...", + help="Use only subset of arches from given repo") + (options, args) = parser.parse_args(args) + activate_session(session, goptions) + if options.mode: + if options.mode not in koji.REPO_MERGE_MODES: + parser.error('Invalid mode: %s' % options.mode) + if not options.tag: + parser.error('The --mode option can only be used with --tag') + if len(args) == 1: + name = args[0] + rinfo = session.getExternalRepo(name, strict=True) + if not options.tag: + parser.error("A url is required to create an external repo entry") + elif len(args) == 2: + name, url = args + rinfo = session.createExternalRepo(name, url) + print("Created external repo %(id)i" % rinfo) + else: + parser.error("Incorrect number of arguments") + if options.tag: + for tagpri in options.tag: + tag, priority = _parse_tagpri(tagpri) + if priority is None: + if options.priority is not None: + priority = options.priority + else: + priority = _pick_external_repo_priority(session, tag) + callopts = {} + if options.mode: + callopts['merge_mode'] = options.mode + if options.arches: + callopts['arches'] = options.arches + session.addExternalRepoToTag(tag, rinfo['name'], priority, **callopts) + print("Added external repo %s to tag %s (priority %i)" + % (rinfo['name'], tag, priority)) + + diff --git a/cli/koji_cli/commands/cmd_add_group.py b/cli/koji_cli/commands/cmd_add_group.py new file mode 100644 index 0000000..d0a28c3 --- /dev/null +++ b/cli/koji_cli/commands/cmd_add_group.py @@ -0,0 +1,39 @@ +from __future__ import absolute_import, division + +from optparse import OptionParser + + + +from koji_cli.lib import ( + activate_session, + error, + get_usage_str +) + + +def handle_add_group(goptions, session, args): + "[admin] Add a group to a tag" + usage = "usage: %prog add-group " + parser = OptionParser(usage=get_usage_str(usage)) + (options, args) = parser.parse_args(args) + if len(args) != 2: + parser.error("Please specify a tag name and a group name") + tag = args[0] + group = args[1] + + activate_session(session, goptions) + if not (session.hasPerm('admin') or session.hasPerm('tag')): + parser.error("This action requires tag or admin privileges") + + dsttag = session.getTag(tag) + if not dsttag: + error("No such tag: %s" % tag) + + groups = dict([(p['name'], p['group_id']) for p in session.getTagGroups(tag, inherit=False)]) + group_id = groups.get(group, None) + if group_id is not None: + error("Group %s already exists for tag %s" % (group, tag)) + + session.groupListAdd(tag, group) + + diff --git a/cli/koji_cli/commands/cmd_add_group_pkg.py b/cli/koji_cli/commands/cmd_add_group_pkg.py new file mode 100644 index 0000000..0bf0b29 --- /dev/null +++ b/cli/koji_cli/commands/cmd_add_group_pkg.py @@ -0,0 +1,26 @@ +from __future__ import absolute_import, division + +from optparse import OptionParser + + + +from koji_cli.lib import ( + activate_session, + get_usage_str +) + + +def handle_add_group_pkg(goptions, session, args): + "[admin] Add a package to a group's package listing" + usage = "usage: %prog add-group-pkg [options] [ ...]" + parser = OptionParser(usage=get_usage_str(usage)) + (options, args) = parser.parse_args(args) + if len(args) < 3: + parser.error("You must specify a tag name, group name, and one or more package names") + tag = args[0] + group = args[1] + activate_session(session, goptions) + for pkg in args[2:]: + session.groupPackageListAdd(tag, group, pkg) + + diff --git a/cli/koji_cli/commands/cmd_add_group_req.py b/cli/koji_cli/commands/cmd_add_group_req.py new file mode 100644 index 0000000..e1b88d6 --- /dev/null +++ b/cli/koji_cli/commands/cmd_add_group_req.py @@ -0,0 +1,26 @@ +from __future__ import absolute_import, division + +from optparse import OptionParser + + + +from koji_cli.lib import ( + activate_session, + get_usage_str +) + + +def handle_add_group_req(goptions, session, args): + "[admin] Add a group to a group's required list" + usage = "usage: %prog add-group-req [options] " + parser = OptionParser(usage=get_usage_str(usage)) + (options, args) = parser.parse_args(args) + if len(args) != 3: + parser.error("You must specify a tag name and two group names") + tag = args[0] + group = args[1] + req = args[2] + activate_session(session, goptions) + session.groupReqListAdd(tag, group, req) + + diff --git a/cli/koji_cli/commands/cmd_add_host.py b/cli/koji_cli/commands/cmd_add_host.py new file mode 100644 index 0000000..e38b904 --- /dev/null +++ b/cli/koji_cli/commands/cmd_add_host.py @@ -0,0 +1,37 @@ +from __future__ import absolute_import, division + +from optparse import OptionParser + + + +from koji_cli.lib import ( + activate_session, + error, + get_usage_str +) + + +def handle_add_host(goptions, session, args): + "[admin] Add a host" + usage = "usage: %prog add-host [options] [ ...]" + parser = OptionParser(usage=get_usage_str(usage)) + parser.add_option("--krb-principal", + help="set a non-default kerberos principal for the host") + parser.add_option("--force", default=False, action="store_true", + help="if existing used is a regular user, convert it to a host") + (options, args) = parser.parse_args(args) + if len(args) < 2: + parser.error("Please specify a hostname and at least one arch") + host = args[0] + activate_session(session, goptions) + id = session.getHost(host) + if id: + error("%s is already in the database" % host) + else: + kwargs = {'force': options.force} + if options.krb_principal is not None: + kwargs['krb_principal'] = options.krb_principal + id = session.addHost(host, args[1:], **kwargs) + print("%s added: id %d" % (host, id)) + + diff --git a/cli/koji_cli/commands/cmd_add_host_to_channel.py b/cli/koji_cli/commands/cmd_add_host_to_channel.py new file mode 100644 index 0000000..ae375f5 --- /dev/null +++ b/cli/koji_cli/commands/cmd_add_host_to_channel.py @@ -0,0 +1,46 @@ +from __future__ import absolute_import, division + +from optparse import SUPPRESS_HELP, OptionParser + + + +from koji_cli.lib import ( + activate_session, + error, + get_usage_str +) + + +def handle_add_host_to_channel(goptions, session, args): + "[admin] Add a host to a channel" + usage = "usage: %prog add-host-to-channel [options] " + parser = OptionParser(usage=get_usage_str(usage)) + parser.add_option("--list", action="store_true", help=SUPPRESS_HELP) + parser.add_option("--new", action="store_true", help="Create channel if needed") + parser.add_option("--force", action="store_true", help="force added, if possible") + (options, args) = parser.parse_args(args) + if not options.list and len(args) != 2: + parser.error("Please specify a hostname and a channel") + activate_session(session, goptions) + if options.list: + for channel in session.listChannels(): + print(channel['name']) + return + channel = args[1] + if not options.new: + channelinfo = session.getChannel(channel) + if not channelinfo: + error("No such channel: %s" % channel) + + host = args[0] + hostinfo = session.getHost(host) + if not hostinfo: + error("No such host: %s" % host) + kwargs = {} + if options.new: + kwargs['create'] = True + if options.force: + kwargs['force'] = True + session.addHostToChannel(host, channel, **kwargs) + + diff --git a/cli/koji_cli/commands/cmd_add_notification.py b/cli/koji_cli/commands/cmd_add_notification.py new file mode 100644 index 0000000..1c688ab --- /dev/null +++ b/cli/koji_cli/commands/cmd_add_notification.py @@ -0,0 +1,57 @@ +from __future__ import absolute_import, division + +from optparse import OptionParser + + +import koji + +from koji_cli.lib import ( + activate_session, + get_usage_str +) + + +def handle_add_notification(goptions, session, args): + "[monitor] Add user's notification" + usage = "usage: %prog add-notification [options]" + parser = OptionParser(usage=get_usage_str(usage)) + parser.add_option("--user", help="Add notifications for this user (admin-only)") + parser.add_option("--package", help="Add notifications for this package") + parser.add_option("--tag", help="Add notifications for this tag") + parser.add_option("--success-only", action="store_true", default=False, help="") + (options, args) = parser.parse_args(args) + + if len(args) != 0: + parser.error("This command takes no arguments") + + if not options.package and not options.tag: + parser.error("Command need at least one from --tag or --package options.") + + activate_session(session, goptions) + + if options.user and not session.hasPerm('admin'): + parser.error("--user requires admin permission") + + if options.user: + user_id = session.getUser(options.user)['id'] + else: + user_id = session.getLoggedInUser()['id'] + + if options.package: + package_id = session.getPackageID(options.package) + if package_id is None: + parser.error("No such package: %s" % options.package) + else: + package_id = None + + if options.tag: + try: + tag_id = session.getTagID(options.tag, strict=True) + except koji.GenericError: + parser.error("No such tag: %s" % options.tag) + else: + tag_id = None + + session.createNotification(user_id, package_id, tag_id, options.success_only) + + diff --git a/cli/koji_cli/commands/cmd_add_pkg.py b/cli/koji_cli/commands/cmd_add_pkg.py new file mode 100644 index 0000000..349e281 --- /dev/null +++ b/cli/koji_cli/commands/cmd_add_pkg.py @@ -0,0 +1,61 @@ +from __future__ import absolute_import, division + +from optparse import OptionParser + + +import koji + +from koji_cli.lib import ( + activate_session, + error, + get_usage_str +) + + +def handle_add_pkg(goptions, session, args): + "[admin] Add a package to the listing for tag" + usage = "usage: %prog add-pkg [options] --owner [ ...]" + parser = OptionParser(usage=get_usage_str(usage)) + parser.add_option("--force", action='store_true', help="Override blocks if necessary") + parser.add_option("--owner", help="Specify owner") + parser.add_option("--extra-arches", help="Specify extra arches") + (options, args) = parser.parse_args(args) + if len(args) < 2: + parser.error("Please specify a tag and at least one package") + if not options.owner: + parser.error("Please specify an owner for the package(s)") + if not session.getUser(options.owner): + error("No such user: %s" % options.owner) + activate_session(session, goptions) + tag = args[0] + opts = {} + opts['force'] = options.force + opts['block'] = False + # check if list of packages exists for that tag already + dsttag = session.getTag(tag) + if dsttag is None: + error("No such tag: %s" % tag) + try: + pkglist = session.listPackages(tagID=dsttag['id'], with_owners=False) + except koji.ParameterError: + # performance option added in 1.25 + pkglist = session.listPackages(tagID=dsttag['id']) + pkglist = dict([(p['package_name'], p['package_id']) for p in pkglist]) + to_add = [] + for package in args[1:]: + package_id = pkglist.get(package, None) + if package_id is not None: + print("Package %s already exists in tag %s" % (package, tag)) + continue + to_add.append(package) + if options.extra_arches: + opts['extra_arches'] = koji.parse_arches(options.extra_arches) + + # add the packages + print("Adding %i packages to tag %s" % (len(to_add), dsttag['name'])) + session.multicall = True + for package in to_add: + session.packageListAdd(tag, package, options.owner, **opts) + session.multiCall(strict=True) + + diff --git a/cli/koji_cli/commands/cmd_add_tag.py b/cli/koji_cli/commands/cmd_add_tag.py new file mode 100644 index 0000000..2293d56 --- /dev/null +++ b/cli/koji_cli/commands/cmd_add_tag.py @@ -0,0 +1,51 @@ +from __future__ import absolute_import, division + +from optparse import OptionParser + + +import koji + +from koji_cli.lib import ( + activate_session, + arg_filter, + get_usage_str +) + + +def handle_add_tag(goptions, session, args): + "[admin] Add a new tag to the database" + usage = "usage: %prog add-tag [options] " + parser = OptionParser(usage=get_usage_str(usage)) + parser.add_option("--parent", help="Set a parent tag with priority 0") + parser.add_option("--arches", help="Specify arches") + parser.add_option("--maven-support", action="store_true", + help="Enable creation of Maven repos for this tag") + parser.add_option("--include-all", action="store_true", + help="Include all packages in this tag when generating Maven repos") + parser.add_option("-x", "--extra", action="append", default=[], metavar="key=value", + help="Set tag extra option") + (options, args) = parser.parse_args(args) + if len(args) != 1: + parser.error("Please specify a name for the tag") + activate_session(session, goptions) + if not (session.hasPerm('admin') or session.hasPerm('tag')): + parser.error("This action requires tag or admin privileges") + opts = {} + if options.parent: + opts['parent'] = options.parent + if options.arches: + opts['arches'] = koji.parse_arches(options.arches) + if options.maven_support: + opts['maven_support'] = True + if options.include_all: + opts['maven_include_all'] = True + if options.extra: + extra = {} + for xopt in options.extra: + key, value = xopt.split('=', 1) + value = arg_filter(value) + extra[key] = value + opts['extra'] = extra + session.createTag(args[0], **opts) + + diff --git a/cli/koji_cli/commands/cmd_add_tag_inheritance.py b/cli/koji_cli/commands/cmd_add_tag_inheritance.py new file mode 100644 index 0000000..98a30ae --- /dev/null +++ b/cli/koji_cli/commands/cmd_add_tag_inheritance.py @@ -0,0 +1,70 @@ +from __future__ import absolute_import, division + +from optparse import OptionParser + + + +from koji_cli.lib import ( + activate_session, + error, + get_usage_str, + warn +) + + +def handle_add_tag_inheritance(goptions, session, args): + """[admin] Add to a tag's inheritance""" + usage = "usage: %prog add-tag-inheritance [options] " + parser = OptionParser(usage=get_usage_str(usage)) + parser.add_option("--priority", help="Specify priority") + parser.add_option("--maxdepth", help="Specify max depth") + parser.add_option("--intransitive", action="store_true", help="Set intransitive") + parser.add_option("--noconfig", action="store_true", help="Set to packages only") + parser.add_option("--pkg-filter", help="Specify the package filter") + parser.add_option("--force", action="store_true", + help="Force adding a parent to a tag that already has that parent tag") + (options, args) = parser.parse_args(args) + + if len(args) != 2: + parser.error("This command takes exctly two argument: a tag name or ID and that tag's " + "new parent name or ID") + + activate_session(session, goptions) + + tag = session.getTag(args[0]) + if not tag: + parser.error("No such tag: %s" % args[0]) + + parent = session.getTag(args[1]) + if not parent: + parser.error("No such tag: %s" % args[1]) + + inheritanceData = session.getInheritanceData(tag['id']) + priority = options.priority and int(options.priority) or 0 + sameParents = [datum for datum in inheritanceData if datum['parent_id'] == parent['id']] + samePriority = [datum for datum in inheritanceData if datum['priority'] == priority] + + if sameParents and not options.force: + warn("Error: You are attempting to add %s as %s's parent even though it already is " + "%s's parent." + % (parent['name'], tag['name'], tag['name'])) + error("Please use --force if this is what you really want to do.") + if samePriority: + error("Error: There is already an active inheritance with that priority on %s, " + "please specify a different priority with --priority." % tag['name']) + + new_data = {} + new_data['parent_id'] = parent['id'] + new_data['priority'] = options.priority or 0 + if options.maxdepth and options.maxdepth.isdigit(): + new_data['maxdepth'] = int(options.maxdepth) + else: + new_data['maxdepth'] = None + new_data['intransitive'] = options.intransitive or False + new_data['noconfig'] = options.noconfig or False + new_data['pkg_filter'] = options.pkg_filter or '' + + inheritanceData.append(new_data) + session.setInheritanceData(tag['id'], inheritanceData) + + diff --git a/cli/koji_cli/commands/cmd_add_target.py b/cli/koji_cli/commands/cmd_add_target.py new file mode 100644 index 0000000..326bd87 --- /dev/null +++ b/cli/koji_cli/commands/cmd_add_target.py @@ -0,0 +1,45 @@ +from __future__ import absolute_import, division + +from optparse import OptionParser + + + +from koji_cli.lib import ( + activate_session, + error, + get_usage_str +) + + +def handle_add_target(goptions, session, args): + "[admin] Create a new build target" + usage = "usage: %prog add-target " + parser = OptionParser(usage=get_usage_str(usage)) + (options, args) = parser.parse_args(args) + if len(args) < 2: + parser.error("Please specify a target name, a build tag, and destination tag") + elif len(args) > 3: + parser.error("Incorrect number of arguments") + name = args[0] + build_tag = args[1] + if len(args) > 2: + dest_tag = args[2] + else: + # most targets have the same name as their destination + dest_tag = name + activate_session(session, goptions) + if not (session.hasPerm('admin') or session.hasPerm('target')): + parser.error("This action requires target or admin privileges") + + chkbuildtag = session.getTag(build_tag) + chkdesttag = session.getTag(dest_tag) + if not chkbuildtag: + error("No such tag: %s" % build_tag) + if not chkbuildtag.get("arches", None): + error("Build tag has no arches: %s" % build_tag) + if not chkdesttag: + error("No such destination tag: %s" % dest_tag) + + session.createBuildTarget(name, build_tag, dest_tag) + + diff --git a/cli/koji_cli/commands/cmd_add_user.py b/cli/koji_cli/commands/cmd_add_user.py new file mode 100644 index 0000000..99727c5 --- /dev/null +++ b/cli/koji_cli/commands/cmd_add_user.py @@ -0,0 +1,34 @@ +from __future__ import absolute_import, division + +from optparse import OptionParser + + +import koji + +from koji_cli.lib import ( + activate_session, + get_usage_str +) + + +def handle_add_user(goptions, session, args): + "[admin] Add a user" + usage = "usage: %prog add-user [options]" + parser = OptionParser(usage=get_usage_str(usage)) + parser.add_option("--principal", help="The Kerberos principal for this user") + parser.add_option("--disable", help="Prohibit logins by this user", action="store_true") + (options, args) = parser.parse_args(args) + if len(args) < 1: + parser.error("You must specify the username of the user to add") + elif len(args) > 1: + parser.error("This command only accepts one argument (username)") + username = args[0] + if options.disable: + status = koji.USER_STATUS['BLOCKED'] + else: + status = koji.USER_STATUS['NORMAL'] + activate_session(session, goptions) + user_id = session.createUser(username, status=status, krb_principal=options.principal) + print("Added user %s (%i)" % (username, user_id)) + + diff --git a/cli/koji_cli/commands/cmd_add_volume.py b/cli/koji_cli/commands/cmd_add_volume.py new file mode 100644 index 0000000..8d2e0c6 --- /dev/null +++ b/cli/koji_cli/commands/cmd_add_volume.py @@ -0,0 +1,29 @@ +from __future__ import absolute_import, division + +from optparse import OptionParser + + + +from koji_cli.lib import ( + activate_session, + error, + get_usage_str +) + + +def handle_add_volume(goptions, session, args): + "[admin] Add a new storage volume" + usage = "usage: %prog add-volume " + parser = OptionParser(usage=get_usage_str(usage)) + (options, args) = parser.parse_args(args) + if len(args) != 1: + parser.error("Command requires exactly one volume-name.") + name = args[0] + volinfo = session.getVolume(name) + if volinfo: + error("Volume %s already exists" % name) + activate_session(session, goptions) + volinfo = session.addVolume(name) + print("Added volume %(name)s with id %(id)i" % volinfo) + + diff --git a/cli/koji_cli/commands/cmd_assign_task.py b/cli/koji_cli/commands/cmd_assign_task.py new file mode 100644 index 0000000..dd87426 --- /dev/null +++ b/cli/koji_cli/commands/cmd_assign_task.py @@ -0,0 +1,50 @@ +from __future__ import absolute_import, division + +from optparse import OptionParser + + +import koji + +from koji_cli.lib import ( + activate_session, + get_usage_str +) + + +def handle_assign_task(goptions, session, args): + "[admin] Assign a task to a host" + usage = 'usage: %prog assign-task ' + parser = OptionParser(usage=get_usage_str(usage)) + parser.add_option('-f', '--force', action='store_true', default=False, + help='force to assign a non-free task') + (options, args) = parser.parse_args(args) + + if len(args) != 2: + parser.error('please specify a task id and a hostname') + else: + task_id = int(args[0]) + hostname = args[1] + + taskinfo = session.getTaskInfo(task_id, request=False) + if taskinfo is None: + raise koji.GenericError("No such task: %s" % task_id) + + hostinfo = session.getHost(hostname) + if hostinfo is None: + raise koji.GenericError("No such host: %s" % hostname) + + force = False + if options.force: + force = True + + activate_session(session, goptions) + if not session.hasPerm('admin'): + parser.error("This action requires admin privileges") + + ret = session.assignTask(task_id, hostname, force) + if ret: + print('assigned task %d to host %s' % (task_id, hostname)) + else: + print('failed to assign task %d to host %s' % (task_id, hostname)) + + diff --git a/cli/koji_cli/commands/cmd_block_group.py b/cli/koji_cli/commands/cmd_block_group.py new file mode 100644 index 0000000..917f8fa --- /dev/null +++ b/cli/koji_cli/commands/cmd_block_group.py @@ -0,0 +1,39 @@ +from __future__ import absolute_import, division + +from optparse import OptionParser + + + +from koji_cli.lib import ( + activate_session, + error, + get_usage_str +) + + +def handle_block_group(goptions, session, args): + "[admin] Block group in tag" + usage = "usage: %prog block-group " + parser = OptionParser(usage=get_usage_str(usage)) + (options, args) = parser.parse_args(args) + if len(args) != 2: + parser.error("Please specify a tag name and a group name") + tag = args[0] + group = args[1] + + activate_session(session, goptions) + if not (session.hasPerm('admin') or session.hasPerm('tag')): + parser.error("This action requires tag or admin privileges") + + dsttag = session.getTag(tag) + if not dsttag: + error("No such tag: %s" % tag) + + groups = dict([(p['name'], p['group_id']) for p in session.getTagGroups(tag, inherit=False)]) + group_id = groups.get(group, None) + if group_id is None: + error("Group %s doesn't exist within tag %s" % (group, tag)) + + session.groupListBlock(tag, group) + + diff --git a/cli/koji_cli/commands/cmd_block_group_pkg.py b/cli/koji_cli/commands/cmd_block_group_pkg.py new file mode 100644 index 0000000..08355c8 --- /dev/null +++ b/cli/koji_cli/commands/cmd_block_group_pkg.py @@ -0,0 +1,28 @@ +from __future__ import absolute_import, division + +from optparse import OptionParser + + + +from koji_cli.lib import ( + activate_session, + get_usage_str +) + + +def handle_block_group_pkg(goptions, session, args): + "[admin] Block a package from a group's package listing" + usage = "usage: %prog block-group-pkg [options] [ ...]" + usage += '\n' + "Note that blocking is propagated through the inheritance chain, so " \ + "it is not exactly the same as package removal." + parser = OptionParser(usage=get_usage_str(usage)) + (options, args) = parser.parse_args(args) + if len(args) < 3: + parser.error("You must specify a tag name, group name, and one or more package names") + tag = args[0] + group = args[1] + activate_session(session, goptions) + for pkg in args[2:]: + session.groupPackageListBlock(tag, group, pkg) + + diff --git a/cli/koji_cli/commands/cmd_block_group_req.py b/cli/koji_cli/commands/cmd_block_group_req.py new file mode 100644 index 0000000..7ecdf91 --- /dev/null +++ b/cli/koji_cli/commands/cmd_block_group_req.py @@ -0,0 +1,26 @@ +from __future__ import absolute_import, division + +from optparse import OptionParser + + + +from koji_cli.lib import ( + activate_session, + get_usage_str +) + + +def handle_block_group_req(goptions, session, args): + "[admin] Block a group's requirement listing" + usage = "usage: %prog block-group-req [options] " + parser = OptionParser(usage=get_usage_str(usage)) + (options, args) = parser.parse_args(args) + if len(args) != 3: + parser.error("You must specify a tag name and two group names") + tag = args[0] + group = args[1] + req = args[2] + activate_session(session, goptions) + session.groupReqListBlock(tag, group, req) + + diff --git a/cli/koji_cli/commands/cmd_block_notification.py b/cli/koji_cli/commands/cmd_block_notification.py new file mode 100644 index 0000000..598883a --- /dev/null +++ b/cli/koji_cli/commands/cmd_block_notification.py @@ -0,0 +1,65 @@ +from __future__ import absolute_import, division + +from optparse import OptionParser + + +import koji + +from koji_cli.lib import ( + activate_session, + get_usage_str +) + + +def handle_block_notification(goptions, session, args): + "[monitor] Block user's notifications" + usage = "usage: %prog block-notification [options]" + parser = OptionParser(usage=get_usage_str(usage)) + parser.add_option("--user", help="Block notifications for this user (admin-only)") + parser.add_option("--package", help="Block notifications for this package") + parser.add_option("--tag", help="Block notifications for this tag") + parser.add_option("--all", action="store_true", help="Block all notification for this user") + (options, args) = parser.parse_args(args) + + if len(args) != 0: + parser.error("This command takes no arguments") + + if not options.package and not options.tag and not options.all: + parser.error("One of --tag, --package or --all must be specified.") + + activate_session(session, goptions) + + if options.user and not session.hasPerm('admin'): + parser.error("--user requires admin permission") + + if options.user: + user_id = session.getUser(options.user, strict=True)['id'] + else: + logged_in_user = session.getLoggedInUser() + if logged_in_user: + user_id = logged_in_user['id'] + else: + parser.error("Please login with authentication or specify --user") + + if options.package: + package_id = session.getPackageID(options.package) + if package_id is None: + parser.error("No such package: %s" % options.package) + else: + package_id = None + + if options.tag: + try: + tag_id = session.getTagID(options.tag, strict=True) + except koji.GenericError: + parser.error("No such tag: %s" % options.tag) + else: + tag_id = None + + for block in session.getBuildNotificationBlocks(user_id): + if block['package_id'] == package_id and block['tag_id'] == tag_id: + parser.error('Notification already exists.') + + session.createNotificationBlock(user_id, package_id, tag_id) + + diff --git a/cli/koji_cli/commands/cmd_block_pkg.py b/cli/koji_cli/commands/cmd_block_pkg.py new file mode 100644 index 0000000..49620d5 --- /dev/null +++ b/cli/koji_cli/commands/cmd_block_pkg.py @@ -0,0 +1,55 @@ +from __future__ import absolute_import, division + +from optparse import OptionParser + + +import koji + +from koji_cli.lib import ( + activate_session, + error, + get_usage_str, + warn +) + + +def handle_block_pkg(goptions, session, args): + "[admin] Block a package in the listing for tag" + usage = "usage: %prog block-pkg [options] [ ...]" + parser = OptionParser(usage=get_usage_str(usage)) + parser.add_option("--force", action='store_true', default=False, + help="Override blocks and owner if necessary") + (options, args) = parser.parse_args(args) + if len(args) < 2: + parser.error("Please specify a tag and at least one package") + activate_session(session, goptions) + tag = args[0] + # check if list of packages exists for that tag already + dsttag = session.getTag(tag) + if dsttag is None: + error("No such tag: %s" % tag) + try: + pkglist = session.listPackages(tagID=dsttag['id'], inherited=True, with_owners=False) + except koji.ParameterError: + # performance option added in 1.25 + pkglist = session.listPackages(tagID=dsttag['id'], inherited=True) + pkglist = dict([(p['package_name'], p['package_id']) for p in pkglist]) + ret = 0 + for package in args[1:]: + package_id = pkglist.get(package, None) + if package_id is None: + warn("Package %s doesn't exist in tag %s" % (package, tag)) + ret = 1 + if ret: + error(code=ret) + session.multicall = True + for package in args[1:]: + # force is not supported on older hub, so use it only explicitly + # https://pagure.io/koji/issue/1388 + if options.force: + session.packageListBlock(tag, package, force=options.force) + else: + session.packageListBlock(tag, package) + session.multiCall(strict=True) + + diff --git a/cli/koji_cli/commands/cmd_build.py b/cli/koji_cli/commands/cmd_build.py new file mode 100644 index 0000000..7f88230 --- /dev/null +++ b/cli/koji_cli/commands/cmd_build.py @@ -0,0 +1,132 @@ +from __future__ import absolute_import, division + +import json +import os +import textwrap +from optparse import OptionParser + + +import koji + +from koji_cli.lib import ( + _progress_callback, + _running_in_bg, + activate_session, + get_usage_str, + unique_path, + watch_tasks +) + + +def handle_build(options, session, args): + "[build] Build a package from source" + + usage = """\ + usage: %prog build [options] + + The first option is the build target, not to be confused with the destination + tag (where the build eventually lands) or build tag (where the buildroot + contents are pulled from). + + You can list all available build targets using the '%prog list-targets' command. + More detail can be found in the documentation. + https://docs.pagure.org/koji/HOWTO/#package-organization""" + + usage = textwrap.dedent(usage) + parser = OptionParser(usage=get_usage_str(usage)) + parser.add_option("--skip-tag", action="store_true", help="Do not attempt to tag package") + parser.add_option("--scratch", action="store_true", help="Perform a scratch build") + parser.add_option("--rebuild-srpm", action="store_true", dest="rebuild_srpm", + help="Force rebuilding SRPM for scratch build only") + parser.add_option("--no-rebuild-srpm", action="store_false", dest="rebuild_srpm", + help="Force not to rebuild srpm for scratch build only") + parser.add_option("--wait", action="store_true", + help="Wait on the build, even if running in the background") + parser.add_option("--nowait", action="store_false", dest="wait", help="Don't wait on build") + parser.add_option("--wait-repo", action="store_true", + help="Wait for the actual buildroot repo of given target") + parser.add_option("--wait-build", metavar="NVR", action="append", dest="wait_builds", + default=[], help="Wait for the given nvr to appear in buildroot repo") + parser.add_option("--quiet", action="store_true", + help="Do not print the task information", default=options.quiet) + parser.add_option("--arch-override", help="Override build arches") + parser.add_option("--fail-fast", action="store_true", + help="Override build_arch_can_fail settings and fail as fast as possible") + parser.add_option("--repo-id", type="int", help="Use a specific repo") + parser.add_option("--noprogress", action="store_true", + help="Do not display progress of the upload") + parser.add_option("--background", action="store_true", + help="Run the build at a lower priority") + parser.add_option("--custom-user-metadata", type="str", + help="Provide a JSON string of custom metadata to be deserialized and " + "stored under the build's extra.custom_user_metadata field") + (build_opts, args) = parser.parse_args(args) + if len(args) != 2: + parser.error("Exactly two arguments (a build target and a SCM URL or srpm file) are " + "required") + if build_opts.rebuild_srpm is not None and not build_opts.scratch: + parser.error("--no-/rebuild-srpm is only allowed for --scratch builds") + if build_opts.arch_override and not build_opts.scratch: + parser.error("--arch_override is only allowed for --scratch builds") + custom_user_metadata = {} + if build_opts.custom_user_metadata: + try: + custom_user_metadata = json.loads(build_opts.custom_user_metadata) + # Use ValueError instead of json.JSONDecodeError for Python 2 and 3 compatibility + except ValueError: + parser.error("--custom-user-metadata is not valid JSON") + + if not isinstance(custom_user_metadata, dict): + parser.error("--custom-user-metadata must be a JSON object") + + activate_session(session, options) + target = args[0] + if target.lower() == "none" and build_opts.repo_id: + target = None + build_opts.skip_tag = True + else: + build_target = session.getBuildTarget(target) + if not build_target: + parser.error("No such build target: %s" % target) + dest_tag = session.getTag(build_target['dest_tag']) + if not dest_tag: + parser.error("No such destination tag: %s" % build_target['dest_tag_name']) + if dest_tag['locked'] and not build_opts.scratch: + parser.error("Destination tag %s is locked" % dest_tag['name']) + source = args[1] + opts = {} + if build_opts.arch_override: + opts['arch_override'] = koji.parse_arches(build_opts.arch_override) + for key in ('skip_tag', 'scratch', 'repo_id', 'fail_fast', 'wait_repo', 'wait_builds', + 'rebuild_srpm'): + val = getattr(build_opts, key) + if val is not None: + opts[key] = val + opts["custom_user_metadata"] = custom_user_metadata + priority = None + if build_opts.background: + # relative to koji.PRIO_DEFAULT + priority = 5 + # try to check that source is an SRPM + if '://' not in source: + # treat source as an srpm and upload it + if not build_opts.quiet: + print("Uploading srpm: %s" % source) + serverdir = unique_path('cli-build') + if _running_in_bg() or build_opts.noprogress or build_opts.quiet: + callback = None + else: + callback = _progress_callback + session.uploadWrapper(source, serverdir, callback=callback) + print('') + source = "%s/%s" % (serverdir, os.path.basename(source)) + task_id = session.build(source, target, opts, priority=priority) + if not build_opts.quiet: + print("Created task: %d" % task_id) + print("Task info: %s/taskinfo?taskID=%s" % (options.weburl, task_id)) + if build_opts.wait or (build_opts.wait is None and not _running_in_bg()): + session.logout() + return watch_tasks(session, [task_id], quiet=build_opts.quiet, + poll_interval=options.poll_interval, topurl=options.topurl) + + diff --git a/cli/koji_cli/commands/cmd_buildinfo.py b/cli/koji_cli/commands/cmd_buildinfo.py new file mode 100644 index 0000000..aada1d3 --- /dev/null +++ b/cli/koji_cli/commands/cmd_buildinfo.py @@ -0,0 +1,119 @@ +from __future__ import absolute_import, division + +import os +from optparse import OptionParser + + +import koji + +from koji_cli.lib import ( + ensure_connection, + error, + get_usage_str, + warn +) + + +def anon_handle_buildinfo(goptions, session, args): + "[info] Print basic information about a build" + usage = "usage: %prog buildinfo [options] [ ...]" + parser = OptionParser(usage=get_usage_str(usage)) + parser.add_option("--changelog", action="store_true", help="Show the changelog for the build") + (options, args) = parser.parse_args(args) + if len(args) < 1: + parser.error("Please specify a build") + ensure_connection(session, goptions) + error_hit = False + for build in args: + if build.isdigit(): + build = int(build) + info = session.getBuild(build) + if info is None: + warn("No such build: %s\n" % build) + error_hit = True + continue + task = None + if info['task_id']: + task = session.getTaskInfo(info['task_id'], request=True) + taglist = [] + for tag in session.listTags(build): + taglist.append(tag['name']) + info['arch'] = 'src' + info['state'] = koji.BUILD_STATES[info['state']] + print("BUILD: %(name)s-%(version)s-%(release)s [%(id)d]" % info) + print("State: %(state)s" % info) + if info['state'] == 'BUILDING': + print("Reserved by: %(cg_name)s" % info) + print("Built by: %(owner_name)s" % info) + source = info.get('source') + if source is not None: + print("Source: %s" % source) + if 'volume_name' in info: + print("Volume: %(volume_name)s" % info) + if task: + print("Task: %s %s" % (task['id'], koji.taskLabel(task))) + else: + print("Task: none") + print("Finished: %s" % koji.formatTimeLong(info['completion_ts'])) + maven_info = session.getMavenBuild(info['id']) + if maven_info: + print("Maven groupId: %s" % maven_info['group_id']) + print("Maven artifactId: %s" % maven_info['artifact_id']) + print("Maven version: %s" % maven_info['version']) + win_info = session.getWinBuild(info['id']) + if win_info: + print("Windows build platform: %s" % win_info['platform']) + print("Tags: %s" % ' '.join(taglist)) + if info.get('extra'): + print("Extra: %(extra)r" % info) + archives_seen = {} + maven_archives = session.listArchives(buildID=info['id'], type='maven') + if maven_archives: + print("Maven archives:") + for archive in maven_archives: + archives_seen.setdefault(archive['id'], 1) + print(os.path.join(koji.pathinfo.mavenbuild(info), + koji.pathinfo.mavenfile(archive))) + win_archives = session.listArchives(buildID=info['id'], type='win') + if win_archives: + print("Windows archives:") + for archive in win_archives: + archives_seen.setdefault(archive['id'], 1) + print(os.path.join(koji.pathinfo.winbuild(info), koji.pathinfo.winfile(archive))) + img_archives = session.listArchives(buildID=info['id'], type='image') + if img_archives: + print('Image archives:') + for archive in img_archives: + archives_seen.setdefault(archive['id'], 1) + print(os.path.join(koji.pathinfo.imagebuild(info), archive['filename'])) + archive_idx = {} + for archive in session.listArchives(buildID=info['id']): + if archive['id'] in archives_seen: + continue + archive_idx.setdefault(archive['btype'], []).append(archive) + for btype in archive_idx: + archives = archive_idx[btype] + print('%s Archives:' % btype.capitalize()) + for archive in archives: + print(os.path.join(koji.pathinfo.typedir(info, btype), archive['filename'])) + rpms = session.listRPMs(buildID=info['id']) + if rpms: + with session.multicall() as mc: + for rpm in rpms: + rpm['sigs'] = mc.queryRPMSigs(rpm['id']) + print("RPMs:") + for rpm in rpms: + line = os.path.join(koji.pathinfo.build(info), koji.pathinfo.rpm(rpm)) + keys = ', '.join(sorted([x['sigkey'] for x in rpm['sigs'].result if x['sigkey']])) + if keys: + line += '\tSignatures: %s' % keys + print(line) + if options.changelog: + changelog = session.getChangelogEntries(info['id']) + if changelog: + print("Changelog:") + print(koji.util.formatChangelog(changelog)) + if error_hit: + error() + + diff --git a/cli/koji_cli/commands/cmd_call.py b/cli/koji_cli/commands/cmd_call.py new file mode 100644 index 0000000..6b85740 --- /dev/null +++ b/cli/koji_cli/commands/cmd_call.py @@ -0,0 +1,61 @@ +from __future__ import absolute_import, division + +import ast +import json +import pprint +import textwrap +from optparse import OptionParser + + + +from koji_cli.lib import ( + DatetimeJSONEncoder, + activate_session, + arg_filter, + get_usage_str +) + + +def handle_call(goptions, session, args): + "Execute an arbitrary XML-RPC call" + usage = """\ + usage: %prog call [options] [ ...] + + Note, that you can use global option --noauth for anonymous calls here""" + usage = textwrap.dedent(usage) + parser = OptionParser(usage=get_usage_str(usage)) + parser.add_option("--python", action="store_true", help="Use python syntax for values") + parser.add_option("--kwargs", + help="Specify keyword arguments as a dictionary (implies --python)") + parser.add_option("--json-output", action="store_true", help="Use JSON syntax for output") + (options, args) = parser.parse_args(args) + if len(args) < 1: + parser.error("Please specify the name of the XML-RPC method") + if options.kwargs: + options.python = True + if options.python and ast is None: + parser.error("The ast module is required to read python syntax") + if options.json_output and json is None: + parser.error("The json module is required to output JSON syntax") + activate_session(session, goptions) + name = args[0] + non_kw = [] + kw = {} + if options.python: + non_kw = [ast.literal_eval(a) for a in args[1:]] + if options.kwargs: + kw = ast.literal_eval(options.kwargs) + else: + for arg in args[1:]: + if arg.find('=') != -1: + key, value = arg.split('=', 1) + kw[key] = arg_filter(value) + else: + non_kw.append(arg_filter(arg)) + response = getattr(session, name).__call__(*non_kw, **kw) + if options.json_output: + print(json.dumps(response, indent=2, separators=(',', ': '), cls=DatetimeJSONEncoder)) + else: + pprint.pprint(response) + + diff --git a/cli/koji_cli/commands/cmd_cancel.py b/cli/koji_cli/commands/cmd_cancel.py new file mode 100644 index 0000000..22989d0 --- /dev/null +++ b/cli/koji_cli/commands/cmd_cancel.py @@ -0,0 +1,62 @@ +from __future__ import absolute_import, division + +from optparse import OptionParser + + +import koji + +from koji_cli.lib import ( + activate_session, + get_usage_str, + warn +) + + +def handle_cancel(goptions, session, args): + "[build] Cancel tasks and/or builds" + usage = "usage: %prog cancel [options] [ ...]" + parser = OptionParser(usage=get_usage_str(usage)) + parser.add_option("--justone", action="store_true", help="Do not cancel subtasks") + parser.add_option("--full", action="store_true", help="Full cancellation (admin only)") + parser.add_option("--force", action="store_true", help="Allow subtasks with --full") + (options, args) = parser.parse_args(args) + if len(args) == 0: + parser.error("You must specify at least one task id or build") + activate_session(session, goptions) + tlist = [] + blist = [] + for arg in args: + try: + tlist.append(int(arg)) + except ValueError: + try: + koji.parse_NVR(arg) + blist.append(arg) + except koji.GenericError: + parser.error("please specify only task ids (integer) or builds (n-v-r)") + + results = [] + with session.multicall(strict=False, batch=100) as m: + if tlist: + opts = {} + remote_fn = m.cancelTask + if options.justone: + opts['recurse'] = False + elif options.full: + remote_fn = m.cancelTaskFull + if options.force: + opts['strict'] = False + for task_id in tlist: + results.append(remote_fn(task_id, **opts)) + for build in blist: + results.append(m.cancelBuild(build)) + + err = False + for r in results: + if isinstance(r.result, dict): + warn(r.result['faultString']) + err = True + if err: + return 1 + + diff --git a/cli/koji_cli/commands/cmd_chain_build.py b/cli/koji_cli/commands/cmd_chain_build.py new file mode 100644 index 0000000..045ddc2 --- /dev/null +++ b/cli/koji_cli/commands/cmd_chain_build.py @@ -0,0 +1,90 @@ +from __future__ import absolute_import, division + +from optparse import OptionParser + + + +from koji_cli.lib import ( + _running_in_bg, + activate_session, + error, + get_usage_str, + warn, + watch_tasks +) + + +def handle_chain_build(options, session, args): + # XXX - replace handle_build with this, once chain-building has gotten testing + "[build] Build one or more packages from source" + usage = "usage: %prog chain-build [options] [ [:] [:] ...]" + parser = OptionParser(usage=get_usage_str(usage)) + parser.add_option("--wait", action="store_true", + help="Wait on build, even if running in the background") + parser.add_option("--nowait", action="store_false", dest="wait", help="Don't wait on build") + parser.add_option("--quiet", action="store_true", + help="Do not print the task information", default=options.quiet) + parser.add_option("--background", action="store_true", + help="Run the build at a lower priority") + (build_opts, args) = parser.parse_args(args) + if len(args) < 2: + parser.error("At least two arguments (a build target and a SCM URL) are required") + activate_session(session, options) + target = args[0] + build_target = session.getBuildTarget(target) + if not build_target: + parser.error("No such build target: %s" % target) + dest_tag = session.getTag(build_target['dest_tag'], strict=True) + if dest_tag['locked']: + parser.error("Destination tag %s is locked" % dest_tag['name']) + + # check that the destination tag is in the inheritance tree of the build tag + # otherwise there is no way that a chain-build can work + ancestors = session.getFullInheritance(build_target['build_tag']) + if dest_tag['id'] not in [build_target['build_tag']] + \ + [ancestor['parent_id'] for ancestor in ancestors]: + warn("Packages in destination tag %(dest_tag_name)s are not inherited by build tag " + "%(build_tag_name)s" % build_target) + error("Target %s is not usable for a chain-build" % build_target['name']) + sources = args[1:] + + src_list = [] + build_level = [] + # src_lists is a list of lists of sources to build. + # each list is block of builds ("build level") which must all be completed + # before the next block begins. Blocks are separated on the command line with ':' + for src in sources: + if src == ':': + if build_level: + src_list.append(build_level) + build_level = [] + elif '://' in src: + # quick check that src might be a url + build_level.append(src) + elif '/' not in src and not src.endswith('.rpm') and len(src.split('-')) >= 3: + # quick check that it looks like a N-V-R + build_level.append(src) + else: + error('"%s" is not a SCM URL or package N-V-R' % src) + if build_level: + src_list.append(build_level) + + if len(src_list) < 2: + parser.error('You must specify at least one dependency between builds with : (colon)\n' + 'If there are no dependencies, use the build command instead') + + priority = None + if build_opts.background: + # relative to koji.PRIO_DEFAULT + priority = 5 + + task_id = session.chainBuild(src_list, target, priority=priority) + if not build_opts.quiet: + print("Created task: %d" % task_id) + print("Task info: %s/taskinfo?taskID=%s" % (options.weburl, task_id)) + if build_opts.wait or (build_opts.wait is None and not _running_in_bg()): + session.logout() + return watch_tasks(session, [task_id], quiet=build_opts.quiet, + poll_interval=options.poll_interval, topurl=options.topurl) + + diff --git a/cli/koji_cli/commands/cmd_clone_tag.py b/cli/koji_cli/commands/cmd_clone_tag.py new file mode 100644 index 0000000..5016bea --- /dev/null +++ b/cli/koji_cli/commands/cmd_clone_tag.py @@ -0,0 +1,539 @@ +from __future__ import absolute_import, division + +import sys +import time +from collections import OrderedDict, defaultdict +from optparse import OptionParser + +import six +import six.moves.xmlrpc_client +from six.moves import zip + +import koji + +from koji_cli.lib import ( + activate_session, + error, + get_usage_str +) + + +def handle_clone_tag(goptions, session, args): + "[admin] Duplicate the contents of one tag onto another tag" + usage = "usage: %prog clone-tag [options] " + usage += "\nclone-tag will create the destination tag if it does not already exist" + parser = OptionParser(usage=get_usage_str(usage)) + parser.add_option('--config', action='store_true', + help="Copy config from the source to the dest tag") + parser.add_option('--groups', action='store_true', help="Copy group information") + parser.add_option('--pkgs', action='store_true', + help="Copy package list from the source to the dest tag") + parser.add_option('--builds', action='store_true', help="Tag builds into the dest tag") + parser.add_option('--all', action='store_true', + help="The same as --config --groups --pkgs --builds") + parser.add_option('--latest-only', action='store_true', + help="Tag only the latest build of each package") + parser.add_option('--inherit-builds', action='store_true', + help="Include all builds inherited into the source tag into the dest tag") + parser.add_option('--ts', type='int', metavar="TIMESTAMP", + help='Clone tag at last event before specific timestamp') + parser.add_option('--no-delete', action='store_false', dest="delete", default=True, + help="Don't delete any existing content in dest tag.") + parser.add_option('--event', type='int', help='Clone tag at a specific event') + parser.add_option('--repo', type='int', help='Clone tag at a specific repo event') + parser.add_option("-v", "--verbose", action="store_true", help="show changes") + parser.add_option("--notify", action="store_true", default=False, + help='Send tagging/untagging notifications') + parser.add_option("-f", "--force", action="store_true", + help="override tag locks if necessary") + parser.add_option("-n", "--test", action="store_true", help="test mode") + parser.add_option("--batch", type='int', default=100, metavar='SIZE', + help="batch size of multicalls [0 to disable, default: %default]") + (options, args) = parser.parse_args(args) + + if len(args) != 2: + parser.error("This command takes two arguments: ") + activate_session(session, goptions) + + if not options.test and not (session.hasPerm('admin') or session.hasPerm('tag')): + parser.error("This action requires tag or admin privileges") + + if args[0] == args[1]: + parser.error('Source and destination tags must be different.') + + if options.batch < 0: + parser.error("batch size must be bigger than zero") + + if options.all: + options.config = options.groups = options.pkgs = options.builds = True + + event = koji.util.eventFromOpts(session, options) or {} + if event: + event['timestr'] = time.asctime(time.localtime(event['ts'])) + print("Cloning at event %(id)i (%(timestr)s)" % event) + + # store tags. + try: + srctag = session.getBuildConfig(args[0], event=event.get('id')) + except koji.GenericError: + parser.error("No such src-tag: %s" % args[0]) + dsttag = session.getTag(args[1]) + if not srctag: + parser.error("No such src-tag: %s" % args[0]) + if (srctag['locked'] and not options.force) \ + or (dsttag and dsttag['locked'] and not options.force): + parser.error("Error: You are attempting to clone from or to a tag which is locked.\n" + "Please use --force if this is what you really want to do.") + + # init debug lists. + chgpkglist = [] + chgbldlist = [] + chggrplist = [] + # case of brand new dst-tag. + if not dsttag: + # create a new tag, copy srctag header. + if not options.test: + if options.config: + session.createTag(args[1], parent=None, arches=srctag['arches'], + perm=srctag['perm_id'], + locked=srctag['locked'], + maven_support=srctag['maven_support'], + maven_include_all=srctag['maven_include_all'], + extra=srctag['extra']) + else: + session.createTag(args[1], parent=None) + # store the new tag, need its assigned id. + newtag = session.getTag(args[1], strict=True) + # get pkglist of src-tag, including inherited packages. + if options.pkgs: + srcpkgs = session.listPackages(tagID=srctag['id'], + inherited=True, + event=event.get('id')) + srcpkgs.sort(key=lambda x: x['package_name']) + if not options.test: + session.multicall = True + for pkgs in srcpkgs: + # for each package add one entry in the new tag. + chgpkglist.append(('[new]', + pkgs['package_name'], + pkgs['blocked'], + pkgs['owner_name'], + pkgs['tag_name'])) + if not options.test: + # add packages. + session.packageListAdd(newtag['name'], + pkgs['package_name'], + owner=pkgs['owner_name'], + block=pkgs['blocked'], + extra_arches=pkgs['extra_arches']) + if not options.test: + _multicall_with_check(session, options.batch) + if options.builds: + # get --all latest builds from src tag + builds = reversed(session.listTagged(srctag['id'], + event=event.get('id'), + inherit=options.inherit_builds, + latest=options.latest_only)) + if not options.test: + session.multicall = True + for build in builds: + # add missing 'name' field. + build['name'] = build['package_name'] + chgbldlist.append(('[new]', + build['package_name'], + build['nvr'], + koji.BUILD_STATES[build['state']], + build['owner_name'], + build['tag_name'])) + # copy latest builds into new tag + if not options.test: + session.tagBuildBypass(newtag['name'], + build, + force=options.force, + notify=options.notify) + if not options.test: + _multicall_with_check(session, options.batch) + if options.groups: + # Copy the group data + srcgroups = session.getTagGroups(srctag['name'], + event=event.get('id')) + if not options.test: + session.multicall = True + for group in srcgroups: + if not options.test: + session.groupListAdd(newtag['name'], group['name']) + for pkg in group['packagelist']: + if not options.test: + session.groupPackageListAdd(newtag['name'], + group['name'], + pkg['package'], + block=pkg['blocked']) + chggrplist.append(('[new]', pkg['package'], group['name'])) + if not options.test: + _multicall_with_check(session, options.batch) + # case of existing dst-tag. + if dsttag: + if options.config and not options.test: + if dsttag['extra']: + session.editTag2(dsttag['id'], remove_extra=list(dsttag['extra'].keys())) + session.editTag2(dsttag['id'], parent=None, arches=srctag['arches'], + perm=srctag['perm_id'], + locked=srctag['locked'], + maven_support=srctag['maven_support'], + maven_include_all=srctag['maven_include_all'], + extra=srctag['extra']) + dsttag = session.getTag(dsttag['id'], strict=True) + + # get fresh list of packages & builds into maps. + srcpkgs = {} + dstpkgs = {} + srcbldsbypkg = defaultdict(OrderedDict) + dstbldsbypkg = defaultdict(OrderedDict) + srcgroups = OrderedDict() + dstgroups = OrderedDict() + # we use OrderedDict so that these indexes preserve the order given to us + if options.pkgs: + for pkg in session.listPackages(tagID=srctag['id'], + inherited=True, + event=event.get('id')): + srcpkgs[pkg['package_name']] = pkg + for pkg in session.listPackages(tagID=dsttag['id'], + inherited=True): + dstpkgs[pkg['package_name']] = pkg + if options.builds: + # listTagged orders builds latest-first + # so reversing that gives us oldest-first + for build in reversed(session.listTagged(srctag['id'], + event=event.get('id'), + inherit=options.inherit_builds, + latest=options.latest_only)): + srcbldsbypkg[build['package_name']][build['nvr']] = build + # get builds in dsttag without inheritance. + # latest=False to get all builds even when latest_only = True, + # so that only the *latest* build per tag will live in. + for build in reversed(session.listTagged(dsttag['id'], + inherit=False, + latest=False)): + dstbldsbypkg[build['package_name']][build['nvr']] = build + if options.groups: + for group in session.getTagGroups(srctag['name'], + event=event.get('id')): + srcgroups[group['name']] = group + for group in session.getTagGroups(dsttag['name']): + dstgroups[group['name']] = group + # construct to-do lists. + paddlist = [] # list containing new packages to be added from src tag + for (package_name, pkg) in six.iteritems(srcpkgs): + if package_name not in dstpkgs: + paddlist.append(pkg) + paddlist.sort(key=lambda x: x['package_name']) + pdellist = [] # list containing packages no more present in dst tag + for (package_name, pkg) in six.iteritems(dstpkgs): + if package_name not in srcpkgs: + pdellist.append(pkg) + pdellist.sort(key=lambda x: x['package_name']) + baddlist = [] # list containing new builds to be added from src tag + bdellist = [] # list containing new builds to be removed from dst tag + if options.delete: + # remove builds for packages that are absent from src tag + for (pkg, dstblds) in six.iteritems(dstbldsbypkg): + if pkg not in srcbldsbypkg: + bdellist.extend(dstblds.values()) + # add and/or remove builds from dst to match src contents and order + for (pkg, srcblds) in six.iteritems(srcbldsbypkg): + dstblds = dstbldsbypkg[pkg] + ablds = [] + dblds = [] + # firstly, deal with extra builds in dst + removed_nvrs = set(dstblds.keys()) - set(srcblds.keys()) + bld_order = srcblds.copy() + if options.delete: + # mark the extra builds for deletion + dnvrs = [] + for (dstnvr, dstbld) in six.iteritems(dstblds): + if dstnvr in removed_nvrs: + dnvrs.append(dstnvr) + dblds.append(dstbld) + # we also remove them from dstblds now so that they do not + # interfere with the order comparison below + for dnvr in dnvrs: + del dstblds[dnvr] + else: + # in the no-delete case, the extra builds should be forced + # to last in the tag + bld_order = OrderedDict() + for (dstnvr, dstbld) in six.iteritems(dstblds): + if dstnvr in removed_nvrs: + bld_order[dstnvr] = dstbld + for (nvr, srcbld) in six.iteritems(srcblds): + bld_order[nvr] = srcbld + # secondly, add builds from src tag and adjust the order + for (nvr, srcbld) in six.iteritems(bld_order): + found = False + out_of_order = [] + # note that dstblds is trimmed as we go, so we are only + # considering the tail corresponding to where we are at + # in the srcblds loop + for (dstnvr, dstbld) in six.iteritems(dstblds): + if nvr == dstnvr: + found = True + break + else: + out_of_order.append(dstnvr) + dblds.append(dstbld) + for dnvr in out_of_order: + del dstblds[dnvr] + # these will be re-added in the proper order later + if found: + # remove it for next pass so we stay aligned with outer + # loop + del dstblds[nvr] + else: + # missing from dst, so we need to add it + ablds.append(srcbld) + baddlist.extend(ablds) + bdellist.extend(dblds) + baddlist.sort(key=lambda x: x['package_name']) + bdellist.sort(key=lambda x: x['package_name']) + + gaddlist = [] # list containing new groups to be added from src tag + for (grpname, group) in six.iteritems(srcgroups): + if grpname not in dstgroups: + gaddlist.append(group) + gdellist = [] # list containing groups to be removed from src tag + for (grpname, group) in six.iteritems(dstgroups): + if grpname not in srcgroups: + gdellist.append(group) + grpchanges = OrderedDict() # dict of changes to make in shared groups + for (grpname, group) in six.iteritems(srcgroups): + if grpname in dstgroups: + dstgroup = dstgroups[grpname] + grpchanges[grpname] = {'adds': [], 'dels': []} + # Store whether group is inherited or not + grpchanges[grpname]['inherited'] = False + if dstgroup['tag_id'] != dsttag['id']: + grpchanges[grpname]['inherited'] = True + srcgrppkglist = [] + dstgrppkglist = [] + for pkg in group['packagelist']: + srcgrppkglist.append(pkg['package']) + for pkg in dstgroups[grpname]['packagelist']: + dstgrppkglist.append(pkg['package']) + for pkg in srcgrppkglist: + if pkg not in dstgrppkglist: + grpchanges[grpname]['adds'].append(pkg) + for pkg in dstgrppkglist: + if pkg not in srcgrppkglist: + grpchanges[grpname]['dels'].append(pkg) + # ADD new packages. + if not options.test: + session.multicall = True + for pkg in paddlist: + chgpkglist.append(('[add]', + pkg['package_name'], + pkg['blocked'], + pkg['owner_name'], + pkg['tag_name'])) + if not options.test: + session.packageListAdd(dsttag['name'], + pkg['package_name'], + owner=pkg['owner_name'], + block=pkg['blocked'], + extra_arches=pkg['extra_arches']) + if not options.test: + _multicall_with_check(session, options.batch) + # DEL builds. To keep the order we should untag builds at first + if not options.test: + session.multicall = True + for build in bdellist: + # don't delete an inherited build. + if build['tag_name'] == dsttag['name']: + # add missing 'name' field + build['name'] = build['package_name'] + chgbldlist.append(('[del]', + build['package_name'], + build['nvr'], + koji.BUILD_STATES[build['state']], + build['owner_name'], + build['tag_name'])) + # go on del builds from new tag. + if not options.test: + session.untagBuildBypass(dsttag['name'], + build, + force=options.force, + notify=options.notify) + if not options.test: + _multicall_with_check(session, options.batch) + # ADD builds. + if not options.test: + session.multicall = True + for build in baddlist: + # add missing 'name' field. + build['name'] = build['package_name'] + chgbldlist.append(('[add]', + build['package_name'], + build['nvr'], + koji.BUILD_STATES[build['state']], + build['owner_name'], + build['tag_name'])) + # copy latest builds into new tag. + if not options.test: + session.tagBuildBypass(dsttag['name'], + build, + force=options.force, + notify=options.notify) + if not options.test: + _multicall_with_check(session, options.batch) + # ADD groups. + if not options.test: + session.multicall = True + for group in gaddlist: + if not options.test: + session.groupListAdd(dsttag['name'], + group['name'], + force=options.force) + for pkg in group['packagelist']: + if not options.test: + session.groupPackageListAdd(dsttag['name'], + group['name'], + pkg['package'], + force=options.force) + chggrplist.append(('[new]', pkg['package'], group['name'])) + if not options.test: + _multicall_with_check(session, options.batch) + # ADD group pkgs. + if not options.test: + session.multicall = True + for group in grpchanges: + for pkg in grpchanges[group]['adds']: + chggrplist.append(('[new]', pkg, group)) + if not options.test: + session.groupPackageListAdd(dsttag['name'], + group, + pkg, + force=options.force) + if not options.test: + _multicall_with_check(session, options.batch) + if options.delete: + # DEL packages + ninhrtpdellist = [] + inhrtpdellist = [] + for pkg in pdellist: + if pkg['tag_name'] == dsttag['name']: + ninhrtpdellist.append(pkg) + else: + inhrtpdellist.append(pkg) + session.multicall = True + # delete only non-inherited packages. + for pkg in ninhrtpdellist: + # check if package have owned builds inside. + session.listTagged(dsttag['name'], + package=pkg['package_name'], + inherit=False) + bump_builds = session.multiCall(batch=options.batch) + if not options.test: + session.multicall = True + for pkg, [builds] in zip(ninhrtpdellist, bump_builds): + if isinstance(builds, dict): + error(builds['faultString']) + # remove all its builds first if there are any. + for build in builds: + # add missing 'name' field. + build['name'] = build['package_name'] + chgbldlist.append(('[del]', + build['package_name'], + build['nvr'], + koji.BUILD_STATES[build['state']], + build['owner_name'], + build['tag_name'])) + # so delete latest build(s) from new tag. + if not options.test: + session.untagBuildBypass(dsttag['name'], + build, + force=options.force, + notify=options.notify) + # now safe to remove package itself since we resolved its builds. + chgpkglist.append(('[del]', + pkg['package_name'], + pkg['blocked'], + pkg['owner_name'], + pkg['tag_name'])) + if not options.test: + session.packageListRemove(dsttag['name'], + pkg['package_name'], + force=False) + # mark as blocked inherited packages. + for pkg in inhrtpdellist: + chgpkglist.append(('[blk]', + pkg['package_name'], + pkg['blocked'], + pkg['owner_name'], + pkg['tag_name'])) + if not options.test: + session.packageListBlock(dsttag['name'], pkg['package_name']) + if not options.test: + _multicall_with_check(session, options.batch) + # DEL groups. + if not options.test: + session.multicall = True + for group in gdellist: + # Only delete a group that isn't inherited + if group['tag_id'] == dsttag['id']: + if not options.test: + session.groupListRemove(dsttag['name'], + group['name'], + force=options.force) + for pkg in group['packagelist']: + chggrplist.append(('[del]', pkg['package'], group['name'])) + # mark as blocked inherited groups. + else: + if not options.test: + session.groupListBlock(dsttag['name'], group['name']) + for pkg in group['packagelist']: + chggrplist.append(('[blk]', pkg['package'], group['name'])) + if not options.test: + _multicall_with_check(session, options.batch) + # DEL group pkgs. + if not options.test: + session.multicall = True + for group in grpchanges: + for pkg in grpchanges[group]['dels']: + # Only delete a group that isn't inherited + if not grpchanges[group]['inherited']: + chggrplist.append(('[del]', pkg, group)) + if not options.test: + session.groupPackageListRemove(dsttag['name'], + group, + pkg, + force=options.force) + else: + chggrplist.append(('[blk]', pkg, group)) + if not options.test: + session.groupPackageListBlock(dsttag['name'], + group, + pkg) + if not options.test: + _multicall_with_check(session, options.batch) + # print final list of actions. + if options.verbose: + pfmt = ' %-7s %-28s %-10s %-10s %-10s\n' + bfmt = ' %-7s %-28s %-40s %-10s %-10s %-10s\n' + gfmt = ' %-7s %-28s %-28s\n' + sys.stdout.write('\nList of changes:\n\n') + sys.stdout.write(pfmt % ('Action', 'Package', 'Blocked', 'Owner', 'From Tag')) + sys.stdout.write(pfmt % ('-' * 7, '-' * 28, '-' * 10, '-' * 10, '-' * 10)) + for changes in chgpkglist: + sys.stdout.write(pfmt % changes) + sys.stdout.write('\n') + sys.stdout.write(bfmt % + ('Action', 'From/To Package', 'Build(s)', 'State', 'Owner', 'From Tag')) + sys.stdout.write(bfmt % ('-' * 7, '-' * 28, '-' * 40, '-' * 10, '-' * 10, '-' * 10)) + for changes in chgbldlist: + sys.stdout.write(bfmt % changes) + sys.stdout.write('\n') + sys.stdout.write(gfmt % ('Action', 'Package', 'Group')) + sys.stdout.write(gfmt % ('-' * 7, '-' * 28, '-' * 28)) + for changes in chggrplist: + sys.stdout.write(gfmt % changes) + + diff --git a/cli/koji_cli/commands/cmd_disable_channel.py b/cli/koji_cli/commands/cmd_disable_channel.py new file mode 100644 index 0000000..f16bd70 --- /dev/null +++ b/cli/koji_cli/commands/cmd_disable_channel.py @@ -0,0 +1,39 @@ +from __future__ import absolute_import, division + +from optparse import OptionParser + +from six.moves import zip + + +from koji_cli.lib import ( + activate_session, + error, + get_usage_str +) + + +def handle_disable_channel(goptions, session, args): + "[admin] Mark one or more channels as disabled" + usage = "usage: %prog disable-channel [options] [ ...]" + parser = OptionParser(usage=get_usage_str(usage)) + parser.add_option("--comment", help="Comment indicating why the channel(s) are being disabled") + (options, args) = parser.parse_args(args) + + if not args: + parser.error("At least one channel must be specified") + + activate_session(session, goptions) + + with session.multicall() as m: + result = [m.getChannel(channel, strict=False) for channel in args] + error_hit = False + for channel, id in zip(args, result): + if not id.result: + print("No such channel: %s" % channel) + error_hit = True + if error_hit: + error("No changes made. Please correct the command line.") + with session.multicall() as m: + [m.disableChannel(channel, comment=options.comment) for channel in args] + + diff --git a/cli/koji_cli/commands/cmd_disable_host.py b/cli/koji_cli/commands/cmd_disable_host.py new file mode 100644 index 0000000..9d64819 --- /dev/null +++ b/cli/koji_cli/commands/cmd_disable_host.py @@ -0,0 +1,43 @@ +from __future__ import absolute_import, division + +from optparse import OptionParser + +from six.moves import zip + + +from koji_cli.lib import ( + activate_session, + error, + get_usage_str +) + + +def handle_disable_host(goptions, session, args): + "[admin] Mark one or more hosts as disabled" + usage = "usage: %prog disable-host [options] [ ...]" + parser = OptionParser(usage=get_usage_str(usage)) + parser.add_option("--comment", help="Comment indicating why the host(s) are being disabled") + (options, args) = parser.parse_args(args) + + if not args: + parser.error("At least one host must be specified") + + activate_session(session, goptions) + session.multicall = True + for host in args: + session.getHost(host) + error_hit = False + for host, [id] in zip(args, session.multiCall(strict=True)): + if not id: + print("No such host: %s" % host) + error_hit = True + if error_hit: + error("No changes made. Please correct the command line.") + session.multicall = True + for host in args: + session.disableHost(host) + if options.comment is not None: + session.editHost(host, comment=options.comment) + session.multiCall(strict=True) + + diff --git a/cli/koji_cli/commands/cmd_disable_user.py b/cli/koji_cli/commands/cmd_disable_user.py new file mode 100644 index 0000000..19217aa --- /dev/null +++ b/cli/koji_cli/commands/cmd_disable_user.py @@ -0,0 +1,26 @@ +from __future__ import absolute_import, division + +from optparse import OptionParser + + + +from koji_cli.lib import ( + activate_session, + get_usage_str +) + + +def handle_disable_user(goptions, session, args): + "[admin] Disable logins by a user" + usage = "usage: %prog disable-user " + parser = OptionParser(usage=get_usage_str(usage)) + (options, args) = parser.parse_args(args) + if len(args) < 1: + parser.error("You must specify the username of the user to disable") + elif len(args) > 1: + parser.error("This command only accepts one argument (username)") + username = args[0] + activate_session(session, goptions) + session.disableUser(username) + + diff --git a/cli/koji_cli/commands/cmd_dist_repo.py b/cli/koji_cli/commands/cmd_dist_repo.py new file mode 100644 index 0000000..e815f61 --- /dev/null +++ b/cli/koji_cli/commands/cmd_dist_repo.py @@ -0,0 +1,167 @@ +from __future__ import absolute_import, division + +import os +from optparse import OptionParser + + +import koji + +from koji_cli.lib import ( + _progress_callback, + _running_in_bg, + activate_session, + unique_path, + warn, + watch_tasks +) + + +def handle_dist_repo(options, session, args): + """Create a yum repo with distribution options""" + usage = "usage: %prog dist-repo [options] [ ...]\n\n" \ + "In normal mode, dist-repo behaves like any other koji task.\n" \ + "Sometimes you want to limit running distRepo tasks per tag to only\n" \ + "one. For such behaviour admin (with 'tag' permission) needs to\n" \ + "modify given tag's extra field 'distrepo.cancel_others' to True'\n" \ + "via 'koji edit-tag -x distrepo.cancel_others=True'\n" + usage += "\n(Specify the --help option for a list of other options)" + parser = OptionParser(usage=usage) + parser.add_option('--allow-missing-signatures', action='store_true', + default=False, + help='For RPMs not signed with a desired key, fall back to the primary copy') + parser.add_option("-a", "--arch", action='append', default=[], + help="Indicate an architecture to consider. The default is all " + "architectures associated with the given tag. This option may " + "be specified multiple times.") + parser.add_option("--with-src", action='store_true', help='Also generate a src repo') + parser.add_option("--split-debuginfo", action='store_true', default=False, + help='Split debuginfo info a separate repo for each arch') + parser.add_option('--comps', help='Include a comps file in the repodata') + parser.add_option('--delta-rpms', metavar='REPO', default=[], action='append', + help='Create delta rpms. REPO can be the id of another dist repo ' + 'or the name of a tag that has a dist repo. May be specified ' + 'multiple times.') + parser.add_option('--event', type='int', help='Use tag content at event') + parser.add_option("--volume", help="Generate repo on given volume") + parser.add_option('--non-latest', dest='latest', default=True, + action='store_false', help='Include older builds, not just the latest') + parser.add_option('--multilib', default=None, metavar="CONFIG", + help='Include multilib packages in the repository using the given ' + 'config file') + parser.add_option("--noinherit", action='store_true', default=False, + help='Do not consider tag inheritance') + parser.add_option("--wait", action="store_true", + help="Wait for the task to complete, even if running in the background") + parser.add_option("--nowait", action="store_false", dest="wait", + help="Do not wait for the task to complete") + parser.add_option('--skip-missing-signatures', action='store_true', default=False, + help='Skip RPMs not signed with the desired key(s)') + parser.add_option('--zck', action='store_true', default=False, + help='Generate zchunk files as well as the standard repodata') + parser.add_option('--zck-dict-dir', action='store', default=None, + help='Directory containing compression dictionaries for use by zchunk ' + '(on builder)') + parser.add_option("--write-signed-rpms", action='store_true', default=False, + help='Write a signed rpms for given tag') + task_opts, args = parser.parse_args(args) + if len(args) < 1: + parser.error('You must provide a tag to generate the repo from') + if len(args) < 2 and not task_opts.allow_missing_signatures: + parser.error('Please specify one or more GPG key IDs (or --allow-missing-signatures)') + if task_opts.allow_missing_signatures and task_opts.skip_missing_signatures: + parser.error('allow_missing_signatures and skip_missing_signatures are mutually exclusive') + activate_session(session, options) + stuffdir = unique_path('cli-dist-repo') + if task_opts.comps: + if not os.path.exists(task_opts.comps): + parser.error('could not find %s' % task_opts.comps) + session.uploadWrapper(task_opts.comps, stuffdir, + callback=_progress_callback) + print('') + task_opts.comps = os.path.join(stuffdir, + os.path.basename(task_opts.comps)) + old_repos = [] + if len(task_opts.delta_rpms) > 0: + for repo in task_opts.delta_rpms: + if repo.isdigit(): + rinfo = session.repoInfo(int(repo), strict=True) + else: + # get dist repo for tag + rinfo = session.getRepo(repo, dist=True) + if not rinfo: + # maybe there is an expired one + rinfo = session.getRepo(repo, + state=koji.REPO_STATES['EXPIRED'], dist=True) + if not rinfo: + parser.error("Can't find repo for tag: %s" % repo) + old_repos.append(rinfo['id']) + tag = args[0] + keys = args[1:] + taginfo = session.getTag(tag) + if not taginfo: + parser.error('No such tag: %s' % tag) + allowed_arches = taginfo['arches'] or '' + if not allowed_arches: + for tag_inh in session.getFullInheritance(tag): + allowed_arches = session.getTag(tag_inh['parent_id'])['arches'] or '' + if allowed_arches: + break + if len(task_opts.arch) == 0: + task_opts.arch = allowed_arches.split() + if not task_opts.arch: + parser.error('No arches given and no arches associated with tag') + else: + for a in task_opts.arch: + if not allowed_arches: + warn('Tag %s has an empty arch list' % taginfo['name']) + elif a not in allowed_arches: + warn('%s is not in the list of tag arches' % a) + if task_opts.multilib: + if not os.path.exists(task_opts.multilib): + parser.error('could not find %s' % task_opts.multilib) + if 'x86_64' in task_opts.arch and 'i686' not in task_opts.arch: + parser.error('The multilib arch (i686) must be included') + if 's390x' in task_opts.arch and 's390' not in task_opts.arch: + parser.error('The multilib arch (s390) must be included') + if 'ppc64' in task_opts.arch and 'ppc' not in task_opts.arch: + parser.error('The multilib arch (ppc) must be included') + session.uploadWrapper(task_opts.multilib, stuffdir, + callback=_progress_callback) + task_opts.multilib = os.path.join(stuffdir, + os.path.basename(task_opts.multilib)) + print('') + if 'noarch' in task_opts.arch: + task_opts.arch.remove('noarch') + if task_opts.with_src and 'src' not in task_opts.arch: + task_opts.arch.append('src') + if not task_opts.arch: + parser.error('No arches left.') + + opts = { + 'arch': task_opts.arch, + 'comps': task_opts.comps, + 'delta': old_repos, + 'event': task_opts.event, + 'volume': task_opts.volume, + 'inherit': not task_opts.noinherit, + 'latest': task_opts.latest, + 'multilib': task_opts.multilib, + 'split_debuginfo': task_opts.split_debuginfo, + 'skip_missing_signatures': task_opts.skip_missing_signatures, + 'allow_missing_signatures': task_opts.allow_missing_signatures, + 'zck': task_opts.zck, + 'zck_dict_dir': task_opts.zck_dict_dir, + 'write_signed_rpms': task_opts.write_signed_rpms, + } + task_id = session.distRepo(tag, keys, **opts) + print("Creating dist repo for tag " + tag) + if task_opts.wait or (task_opts.wait is None and not _running_in_bg()): + session.logout() + return watch_tasks(session, [task_id], quiet=options.quiet, + poll_interval=options.poll_interval, topurl=options.topurl) + + +_search_types = ('package', 'build', 'tag', 'target', 'user', 'host', 'rpm', + 'maven', 'win') + + diff --git a/cli/koji_cli/commands/cmd_download_build.py b/cli/koji_cli/commands/cmd_download_build.py new file mode 100644 index 0000000..ee83ed4 --- /dev/null +++ b/cli/koji_cli/commands/cmd_download_build.py @@ -0,0 +1,132 @@ +from __future__ import absolute_import, division + +from optparse import OptionParser + +from six.moves import zip + +import koji + +from koji_cli.lib import ( + download_archive, + download_rpm, + ensure_connection, + error, + get_usage_str, + warn +) + + +def anon_handle_download_build(options, session, args): + "[download] Download a built package" + usage = "usage: %prog download-build [options] " + parser = OptionParser(usage=get_usage_str(usage)) + parser.add_option("--arch", "-a", dest="arches", metavar="ARCH", action="append", default=[], + help="Only download packages for this arch (may be used multiple times)") + parser.add_option("--type", + help="Download archives of the given type, rather than rpms " + "(maven, win, image, remote-sources)") + parser.add_option("--latestfrom", dest="latestfrom", + help="Download the latest build from this tag") + parser.add_option("--debuginfo", action="store_true", help="Also download -debuginfo rpms") + parser.add_option("--task-id", action="store_true", help="Interperet id as a task id") + parser.add_option("--rpm", action="store_true", help="Download the given rpm") + parser.add_option("--key", help="Download rpms signed with the given key") + parser.add_option("--topurl", metavar="URL", default=options.topurl, + help="URL under which Koji files are accessible") + parser.add_option("--noprogress", action="store_true", help="Do not display progress meter") + parser.add_option("-q", "--quiet", action="store_true", + help="Suppress output", default=options.quiet) + (suboptions, args) = parser.parse_args(args) + if len(args) < 1: + parser.error("Please specify a package N-V-R or build ID") + elif len(args) > 1: + parser.error("Only a single package N-V-R or build ID may be specified") + + ensure_connection(session, options) + build = args[0] + + if build.isdigit(): + if suboptions.latestfrom: + parser.error("--latestfrom not compatible with build IDs, specify a package name.") + build = int(build) + if suboptions.task_id: + builds = session.listBuilds(taskID=build) + if not builds: + error("No associated builds for task %s" % build) + build = builds[0]['build_id'] + + if suboptions.latestfrom: + # We want the latest build, not a specific build + try: + builds = session.listTagged(suboptions.latestfrom, latest=True, package=build, + type=suboptions.type) + except koji.GenericError as data: + error("Error finding latest build: %s" % data) + if not builds: + error("%s has no builds of %s" % (suboptions.latestfrom, build)) + info = builds[0] + elif suboptions.rpm: + rpminfo = session.getRPM(build) + if rpminfo is None: + error("No such rpm: %s" % build) + info = session.getBuild(rpminfo['build_id']) + else: + # if we're given an rpm name without --rpm, download the containing build + try: + koji.parse_NVRA(build) + rpminfo = session.getRPM(build) + build = rpminfo['build_id'] + except Exception: + pass + info = session.getBuild(build) + + if info is None: + error("No such build: %s" % build) + + if not suboptions.topurl: + error("You must specify --topurl to download files") + + archives = [] + rpms = [] + if suboptions.type: + archives = session.listArchives(buildID=info['id'], type=suboptions.type) + if not archives: + error("No %s archives available for %s" % (suboptions.type, koji.buildLabel(info))) + else: + arches = suboptions.arches + if len(arches) == 0: + arches = None + if suboptions.rpm: + all_rpms = [rpminfo] + else: + all_rpms = session.listRPMs(buildID=info['id'], arches=arches) + if not all_rpms: + if arches: + error("No %s packages available for %s" % + (" or ".join(arches), koji.buildLabel(info))) + else: + error("No packages available for %s" % koji.buildLabel(info)) + for rpm in all_rpms: + if not suboptions.debuginfo and koji.is_debuginfo(rpm['name']): + continue + rpms.append(rpm) + + if suboptions.key: + with session.multicall() as m: + results = [m.queryRPMSigs(rpm_id=r['id'], sigkey=suboptions.key) for r in rpms] + rpm_keys = [x.result for x in results] + for rpm, rpm_key in list(zip(rpms, rpm_keys)): + if not rpm_key: + nvra = "%(nvr)s-%(arch)s.rpm" % rpm + warn("No such sigkey %s for rpm %s" % (suboptions.key, nvra)) + rpms.remove(rpm) + + # run the download + for rpm in rpms: + download_rpm(info, rpm, suboptions.topurl, sigkey=suboptions.key, + quiet=suboptions.quiet, noprogress=suboptions.noprogress) + for archive in archives: + download_archive(info, archive, suboptions.topurl, + quiet=suboptions.quiet, noprogress=suboptions.noprogress) + + diff --git a/cli/koji_cli/commands/cmd_download_logs.py b/cli/koji_cli/commands/cmd_download_logs.py new file mode 100644 index 0000000..aa6b9ce --- /dev/null +++ b/cli/koji_cli/commands/cmd_download_logs.py @@ -0,0 +1,168 @@ +from __future__ import absolute_import, division + +import os +import pprint +import sys +import traceback +from optparse import OptionParser + +import six +import six.moves.xmlrpc_client + +import koji + +from koji_cli.lib import ( + download_file, + ensure_connection, + error, + get_usage_str, + list_task_output_all_volumes, + warn +) + + +def anon_handle_download_logs(options, session, args): + "[download] Download logs for task" + + FAIL_LOG = "task_failed.log" + usage = "usage: %prog download-logs [options] [ ...]" + usage += "\n %prog download-logs [options] --nvr [ ...]" + usage += "\n" + usage += "\n" + usage += "Note this command only downloads task logs, not build logs." + usage += "\n" + parser = OptionParser(usage=get_usage_str(usage)) + parser.add_option("-r", "--recurse", action="store_true", + help="Process children of this task as well") + parser.add_option("--nvr", action="store_true", + help="Get the logs for the task associated with this build " + "Name-Version-Release.") + parser.add_option("-m", "--match", action="append", metavar="PATTERN", + help="Get only log filenames matching PATTERN (fnmatch). " + "May be used multiple times.") + parser.add_option("-c", "--continue", action="store_true", dest="cont", + help="Continue previous download") + parser.add_option("-d", "--dir", metavar="DIRECTORY", default='kojilogs', + help="Write logs to DIRECTORY") + (suboptions, args) = parser.parse_args(args) + + if len(args) < 1: + parser.error("Please specify at least one task id or n-v-r") + + def write_fail_log(task_log_dir, task_id): + """Gets output only from failed tasks""" + try: + result = session.getTaskResult(task_id) + # with current code, failed task results should always be faults, + # but that could change in the future + content = pprint.pformat(result) + except (six.moves.xmlrpc_client.Fault, koji.GenericError): + etype, e = sys.exc_info()[:2] + content = ''.join(traceback.format_exception_only(etype, e)) + full_filename = os.path.normpath(os.path.join(task_log_dir, FAIL_LOG)) + koji.ensuredir(os.path.dirname(full_filename)) + sys.stdout.write("Writing: %s\n" % full_filename) + with open(full_filename, 'wt') as fo: + fo.write(content) + + def download_log(task_log_dir, task_id, filename, blocksize=102400, volume=None): + # Create directories only if there is any log file to write to + # For each non-default volume create special sub-directory + if volume not in (None, 'DEFAULT'): + full_filename = os.path.normpath(os.path.join(task_log_dir, volume, filename)) + else: + full_filename = os.path.normpath(os.path.join(task_log_dir, filename)) + koji.ensuredir(os.path.dirname(full_filename)) + contents = 'IGNORE ME!' + if suboptions.cont and os.path.exists(full_filename): + sys.stdout.write("Continuing: %s\n" % full_filename) + fd = open(full_filename, 'ab') + offset = fd.tell() + else: + sys.stdout.write("Downloading: %s\n" % full_filename) + fd = open(full_filename, 'wb') + offset = 0 + try: + while contents: + contents = session.downloadTaskOutput(task_id, filename, offset=offset, + size=blocksize, volume=volume) + offset += len(contents) + if contents: + fd.write(contents) + finally: + fd.close() + + def save_logs(task_id, match, parent_dir='.', recurse=True): + assert task_id == int(task_id), "Task id must be number: %r" % task_id + task_info = session.getTaskInfo(task_id) + if task_info is None: + error("No such task: %d" % task_id) + files = list_task_output_all_volumes(session, task_id) + logs = [] # list of tuples (filename, volume) + for filename in files: + if not filename.endswith(".log"): + continue + if match and not koji.util.multi_fnmatch(filename, match): + continue + logs += [(filename, volume) for volume in files[filename]] + + task_log_dir = os.path.join(parent_dir, + "%s-%s" % (task_info["arch"], task_id)) + + count = 0 + state = koji.TASK_STATES[task_info['state']] + if state == 'FAILED': + if not match or koji.util.multi_fnmatch(FAIL_LOG, match): + write_fail_log(task_log_dir, task_id) + count += 1 + elif state not in ['CLOSED', 'CANCELED']: + warn("Task %s is %s\n" % (task_id, state)) + + for log_filename, log_volume in logs: + download_log(task_log_dir, task_id, log_filename, volume=log_volume) + count += 1 + + if count == 0 and not recurse: + warn("No logs found for task %i. Perhaps try --recurse?\n" % task_id) + + if recurse: + child_tasks = session.getTaskChildren(task_id) + for child_task in child_tasks: + save_logs(child_task['id'], match, task_log_dir, recurse) + + ensure_connection(session, options) + task_id = None + build_id = None + for arg in args: + if suboptions.nvr: + suboptions.recurse = True + binfo = session.getBuild(arg) + if binfo is None: + error("There is no build with n-v-r: %s" % arg) + if binfo.get('task_id'): + task_id = binfo['task_id'] + sys.stdout.write("Using task ID: %s\n" % task_id) + elif binfo.get('build_id'): + build_id = binfo['build_id'] + sys.stdout.write("Using build ID: %s\n" % build_id) + else: + try: + task_id = int(arg) + except ValueError: + error("Task id must be number: %r" % arg) + if task_id: + save_logs(task_id, suboptions.match, suboptions.dir, suboptions.recurse) + elif build_id: + logs = session.getBuildLogs(build_id) + match = suboptions.match + for log in logs: + url = os.path.join(options.topurl, log['path']) + filepath = os.path.join(os.getcwd(), '%s/%s/%s' % (suboptions.dir, + arg, log['name'])) + if not filepath.endswith(".log"): + continue + if match and not koji.util.multi_fnmatch(log['name'], match): + continue + download_file(url, filepath) + + diff --git a/cli/koji_cli/commands/cmd_download_task.py b/cli/koji_cli/commands/cmd_download_task.py new file mode 100644 index 0000000..c8b4cb3 --- /dev/null +++ b/cli/koji_cli/commands/cmd_download_task.py @@ -0,0 +1,122 @@ +from __future__ import absolute_import, division + +import os +from optparse import OptionParser + +from six.moves import filter + +import koji + +from koji_cli.lib import ( + _running_in_bg, + download_file, + ensure_connection, + error, + get_usage_str, + list_task_output_all_volumes, + watch_tasks +) + + +def anon_handle_download_task(options, session, args): + "[download] Download the output of a build task" + usage = "usage: %prog download-task " + parser = OptionParser(usage=get_usage_str(usage)) + parser.add_option("--arch", dest="arches", metavar="ARCH", action="append", default=[], + help="Only download packages for this arch (may be used multiple times)") + parser.add_option("--logs", dest="logs", action="store_true", default=False, + help="Also download build logs") + parser.add_option("--topurl", metavar="URL", default=options.topurl, + help="URL under which Koji files are accessible") + parser.add_option("--noprogress", action="store_true", help="Do not display progress meter") + parser.add_option("--wait", action="store_true", + help="Wait for running tasks to finish, even if running in the background") + parser.add_option("--nowait", action="store_false", dest="wait", + help="Do not wait for running tasks to finish") + parser.add_option("-q", "--quiet", action="store_true", + help="Suppress output", default=options.quiet) + + (suboptions, args) = parser.parse_args(args) + if len(args) == 0: + parser.error("Please specify a task ID") + elif len(args) > 1: + parser.error("Only one task ID may be specified") + + base_task_id = int(args.pop()) + if len(suboptions.arches) > 0: + suboptions.arches = ",".join(suboptions.arches).split(",") + + ensure_connection(session, options) + + # get downloadable tasks + + base_task = session.getTaskInfo(base_task_id) + if not base_task: + error('No such task: %d' % base_task_id) + + if (suboptions.wait or (suboptions.wait is None and not _running_in_bg())) and \ + base_task['state'] not in ( + koji.TASK_STATES['CLOSED'], + koji.TASK_STATES['CANCELED'], + koji.TASK_STATES['FAILED']): + watch_tasks(session, [base_task_id], quiet=suboptions.quiet, + poll_interval=options.poll_interval, topurl=options.topurl) + + def check_downloadable(task): + return task["method"] == "buildArch" + + downloadable_tasks = [] + + if check_downloadable(base_task): + downloadable_tasks.append(base_task) + else: + subtasks = session.getTaskChildren(base_task_id) + downloadable_tasks.extend(list(filter(check_downloadable, subtasks))) + + # get files for download + downloads = [] + + for task in downloadable_tasks: + files = list_task_output_all_volumes(session, task["id"]) + for filename in files: + if filename.endswith(".rpm"): + for volume in files[filename]: + filearch = filename.split(".")[-2] + if len(suboptions.arches) == 0 or filearch in suboptions.arches: + downloads.append((task, filename, volume, filename)) + elif filename.endswith(".log") and suboptions.logs: + for volume in files[filename]: + # rename logs, they would conflict + new_filename = "%s.%s.log" % (filename.rstrip(".log"), task["arch"]) + downloads.append((task, filename, volume, new_filename)) + + if len(downloads) == 0: + error("No files for download found.") + + required_tasks = {} + for (task, nop, nop, nop) in downloads: + if task["id"] not in required_tasks: + required_tasks[task["id"]] = task + + for task_id in required_tasks: + if required_tasks[task_id]["state"] != koji.TASK_STATES.get("CLOSED"): + if task_id == base_task_id: + error("Task %d has not finished yet." % task_id) + else: + error("Child task %d has not finished yet." % task_id) + + # perform the download + number = 0 + pathinfo = koji.PathInfo(topdir=suboptions.topurl) + for (task, filename, volume, new_filename) in downloads: + number += 1 + if volume not in (None, 'DEFAULT'): + koji.ensuredir(volume) + new_filename = os.path.join(volume, new_filename) + if '..' in filename: + error('Invalid file name: %s' % filename) + url = '%s/%s/%s' % (pathinfo.work(volume), pathinfo.taskrelpath(task["id"]), filename) + download_file(url, new_filename, quiet=suboptions.quiet, noprogress=suboptions.noprogress, + size=len(downloads), num=number) + + diff --git a/cli/koji_cli/commands/cmd_edit_channel.py b/cli/koji_cli/commands/cmd_edit_channel.py new file mode 100644 index 0000000..e0cd9a2 --- /dev/null +++ b/cli/koji_cli/commands/cmd_edit_channel.py @@ -0,0 +1,46 @@ +from __future__ import absolute_import, division + +from optparse import OptionParser + + +import koji + +from koji_cli.lib import ( + activate_session, + error, + get_usage_str +) + + +def handle_edit_channel(goptions, session, args): + "[admin] Edit a channel" + usage = "usage: %prog edit-channel [options] " + parser = OptionParser(usage=get_usage_str(usage)) + parser.add_option("--name", help="New channel name") + parser.add_option("--description", help="Description of channel") + parser.add_option("--comment", help="Comment of channel") + (options, args) = parser.parse_args(args) + if len(args) != 1: + parser.error("Incorrect number of arguments") + activate_session(session, goptions) + vals = {} + for key, val in options.__dict__.items(): + if val is not None: + vals[key] = val + cinfo = session.getChannel(args[0]) + if not cinfo: + error("No such channel: %s" % args[0]) + try: + result = session.editChannel(args[0], **vals) + except koji.GenericError as ex: + msg = str(ex) + if 'Invalid method:' in msg: + version = session.getKojiVersion() + error("editChannel is available on hub from Koji 1.26 version, your version is %s" % + version) + else: + print(msg) + if not result: + error("No changes made, please correct the command line") + + diff --git a/cli/koji_cli/commands/cmd_edit_external_repo.py b/cli/koji_cli/commands/cmd_edit_external_repo.py new file mode 100644 index 0000000..7824078 --- /dev/null +++ b/cli/koji_cli/commands/cmd_edit_external_repo.py @@ -0,0 +1,62 @@ +from __future__ import absolute_import, division + +from optparse import OptionParser + + +import koji + +from koji_cli.lib import ( + activate_session, + get_usage_str +) + + +def handle_edit_external_repo(goptions, session, args): + "[admin] Edit data for an external repo" + usage = "usage: %prog edit-external-repo [options] " + parser = OptionParser(usage=get_usage_str(usage)) + parser.add_option("--url", help="Change the url") + parser.add_option("--name", help="Change the name") + parser.add_option("-t", "--tag", metavar="TAG", help="Edit the repo properties for the tag.") + parser.add_option("-p", "--priority", metavar="PRIORITY", type='int', + help="Edit the priority of the repo for the tag specified by --tag.") + parser.add_option("-m", "--mode", metavar="MODE", + help="Edit the merge mode of the repo for the tag specified by --tag. " + "Options: %s." % ", ".join(koji.REPO_MERGE_MODES)) + parser.add_option("-a", "--arches", metavar="ARCH1,ARCH2, ...", + help="Use only subset of arches from given repo") + (options, args) = parser.parse_args(args) + if len(args) != 1: + parser.error("Incorrect number of arguments") + repo_opts = {} + if options.url: + repo_opts['url'] = options.url + if options.name: + repo_opts['name'] = options.name + tag_repo_opts = {} + if options.tag: + if options.priority is not None: + tag_repo_opts['priority'] = options.priority + if options.mode: + tag_repo_opts['merge_mode'] = options.mode + if options.arches is not None: + tag_repo_opts['arches'] = options.arches + if not tag_repo_opts: + parser.error("At least, one of priority and merge mode should be specified") + tag_repo_opts['tag_info'] = options.tag + tag_repo_opts['repo_info'] = args[0] + else: + for k in ('priority', 'mode', 'arches'): + if getattr(options, k) is not None: + parser.error("If %s is specified, --tag must be specified as well" % k) + + if not (repo_opts or tag_repo_opts): + parser.error("No changes specified") + + activate_session(session, goptions) + if repo_opts: + session.editExternalRepo(args[0], **repo_opts) + if tag_repo_opts: + session.editTagExternalRepo(**tag_repo_opts) + + diff --git a/cli/koji_cli/commands/cmd_edit_host.py b/cli/koji_cli/commands/cmd_edit_host.py new file mode 100644 index 0000000..00e9f04 --- /dev/null +++ b/cli/koji_cli/commands/cmd_edit_host.py @@ -0,0 +1,60 @@ +from __future__ import absolute_import, division + +from optparse import OptionParser + +from six.moves import zip + +import koji + +from koji_cli.lib import ( + activate_session, + error, + get_usage_str, + warn +) + + +def handle_edit_host(options, session, args): + "[admin] Edit a host" + usage = "usage: %prog edit-host [ ...] [options]" + parser = OptionParser(usage=get_usage_str(usage)) + parser.add_option("--arches", + help="Space or comma-separated list of supported architectures") + parser.add_option("--capacity", type="float", help="Capacity of this host") + parser.add_option("--description", metavar="DESC", help="Description of this host") + parser.add_option("--comment", help="A brief comment about this host") + (subopts, args) = parser.parse_args(args) + if not args: + parser.error("Please specify a hostname") + + activate_session(session, options) + + vals = {} + for key, val in subopts.__dict__.items(): + if val is not None: + vals[key] = val + if 'arches' in vals: + vals['arches'] = koji.parse_arches(vals['arches']) + + session.multicall = True + for host in args: + session.getHost(host) + error_hit = False + for host, [info] in zip(args, session.multiCall(strict=True)): + if not info: + warn("No such host: %s" % host) + error_hit = True + + if error_hit: + error("No changes made, please correct the command line") + + session.multicall = True + for host in args: + session.editHost(host, **vals) + for host, [result] in zip(args, session.multiCall(strict=True)): + if result: + print("Edited %s" % host) + else: + print("No changes made to %s" % host) + + diff --git a/cli/koji_cli/commands/cmd_edit_notification.py b/cli/koji_cli/commands/cmd_edit_notification.py new file mode 100644 index 0000000..76b3631 --- /dev/null +++ b/cli/koji_cli/commands/cmd_edit_notification.py @@ -0,0 +1,67 @@ +from __future__ import absolute_import, division + +from optparse import OptionParser + + +import koji + +from koji_cli.lib import ( + activate_session, + get_usage_str +) + + +def handle_edit_notification(goptions, session, args): + "[monitor] Edit user's notification" + usage = "usage: %prog edit-notification [options] " + parser = OptionParser(usage=get_usage_str(usage)) + parser.add_option("--package", help="Notifications for this package, '*' for all") + parser.add_option("--tag", help="Notifications for this tag, '*' for all") + parser.add_option("--success-only", action="store_true", default=None, + dest='success_only', help="Notify only on successful events") + parser.add_option("--no-success-only", action="store_false", + default=None, dest='success_only', help="Notify on all events") + (options, args) = parser.parse_args(args) + + if len(args) != 1: + parser.error("Only argument is notification ID") + + try: + n_id = int(args[0]) + except ValueError: + parser.error("Notification ID has to be numeric") + + if not options.package and not options.tag and options.success_only is None: + parser.error("Command need at least one option") + + activate_session(session, goptions) + + old = session.getBuildNotification(n_id) + + if options.package == '*': + package_id = None + elif options.package: + package_id = session.getPackageID(options.package) + if package_id is None: + parser.error("No such package: %s" % options.package) + else: + package_id = old['package_id'] + + if options.tag == '*': + tag_id = None + elif options.tag: + try: + tag_id = session.getTagID(options.tag, strict=True) + except koji.GenericError: + parser.error("No such tag: %s" % options.tag) + else: + tag_id = old['tag_id'] + + if options.success_only is not None: + success_only = options.success_only + else: + success_only = old['success_only'] + + session.updateNotification(n_id, package_id, tag_id, success_only) + + diff --git a/cli/koji_cli/commands/cmd_edit_permission.py b/cli/koji_cli/commands/cmd_edit_permission.py new file mode 100644 index 0000000..f40fec6 --- /dev/null +++ b/cli/koji_cli/commands/cmd_edit_permission.py @@ -0,0 +1,25 @@ +from __future__ import absolute_import, division + +from optparse import OptionParser + + + +from koji_cli.lib import ( + activate_session, + get_usage_str +) + + +def handle_edit_permission(goptions, session, args): + "[admin] Edit a permission description" + usage = "usage: %prog edit-permission " + parser = OptionParser(usage=get_usage_str(usage)) + (options, args) = parser.parse_args(args) + if len(args) < 2: + parser.error("Please specify a permission and a description") + activate_session(session, goptions) + perm = args[0] + description = args[1] + session.editPermission(perm, description) + + diff --git a/cli/koji_cli/commands/cmd_edit_tag.py b/cli/koji_cli/commands/cmd_edit_tag.py new file mode 100644 index 0000000..a3d7263 --- /dev/null +++ b/cli/koji_cli/commands/cmd_edit_tag.py @@ -0,0 +1,78 @@ +from __future__ import absolute_import, division + +from optparse import OptionParser + + +import koji + +from koji_cli.lib import ( + activate_session, + arg_filter, + get_usage_str +) + + +def handle_edit_tag(goptions, session, args): + "[admin] Alter tag information" + usage = "usage: %prog edit-tag [options] " + parser = OptionParser(usage=get_usage_str(usage)) + parser.add_option("--arches", help="Specify arches") + parser.add_option("--perm", help="Specify permission requirement") + parser.add_option("--no-perm", action="store_true", help="Remove permission requirement") + parser.add_option("--lock", action="store_true", help="Lock the tag") + parser.add_option("--unlock", action="store_true", help="Unlock the tag") + parser.add_option("--rename", help="Rename the tag") + parser.add_option("--maven-support", action="store_true", + help="Enable creation of Maven repos for this tag") + parser.add_option("--no-maven-support", action="store_true", + help="Disable creation of Maven repos for this tag") + parser.add_option("--include-all", action="store_true", + help="Include all packages in this tag when generating Maven repos") + parser.add_option("--no-include-all", action="store_true", + help="Do not include all packages in this tag when generating Maven repos") + parser.add_option("-x", "--extra", action="append", default=[], metavar="key=value", + help="Set tag extra option. JSON-encoded or simple value") + parser.add_option("-r", "--remove-extra", action="append", default=[], metavar="key", + help="Remove tag extra option") + parser.add_option("-b", "--block-extra", action="append", default=[], metavar="key", + help="Block inherited tag extra option") + (options, args) = parser.parse_args(args) + if len(args) != 1: + parser.error("Please specify a name for the tag") + activate_session(session, goptions) + tag = args[0] + opts = {} + if options.arches: + opts['arches'] = koji.parse_arches(options.arches) + if options.no_perm: + opts['perm'] = None + elif options.perm: + opts['perm'] = options.perm + if options.unlock: + opts['locked'] = False + if options.lock: + opts['locked'] = True + if options.rename: + opts['name'] = options.rename + if options.maven_support: + opts['maven_support'] = True + if options.no_maven_support: + opts['maven_support'] = False + if options.include_all: + opts['maven_include_all'] = True + if options.no_include_all: + opts['maven_include_all'] = False + if options.extra: + extra = {} + for xopt in options.extra: + key, value = xopt.split('=', 1) + if key in extra: + parser.error("Duplicate extra key: %s" % key) + extra[key] = arg_filter(value, parse_json=True) + opts['extra'] = extra + opts['remove_extra'] = options.remove_extra + opts['block_extra'] = options.block_extra + # XXX change callname + session.editTag2(tag, **opts) + + diff --git a/cli/koji_cli/commands/cmd_edit_tag_inheritance.py b/cli/koji_cli/commands/cmd_edit_tag_inheritance.py new file mode 100644 index 0000000..3a19bae --- /dev/null +++ b/cli/koji_cli/commands/cmd_edit_tag_inheritance.py @@ -0,0 +1,94 @@ +from __future__ import absolute_import, division + +from optparse import OptionParser + + + +from koji_cli.lib import ( + activate_session, + error, + get_usage_str +) + + +def handle_edit_tag_inheritance(goptions, session, args): + """[admin] Edit tag inheritance""" + usage = "usage: %prog edit-tag-inheritance [options] " + parser = OptionParser(usage=get_usage_str(usage)) + parser.add_option("--priority", help="Specify a new priority") + parser.add_option("--maxdepth", help="Specify max depth") + parser.add_option("--intransitive", action="store_true", help="Set intransitive") + parser.add_option("--noconfig", action="store_true", help="Set to packages only") + parser.add_option("--pkg-filter", help="Specify the package filter") + (options, args) = parser.parse_args(args) + + if len(args) < 1: + parser.error("This command takes at least one argument: a tag name or ID") + + if len(args) > 3: + parser.error("This command takes at most three argument: a tag name or ID, " + "a parent tag name or ID, and a priority") + + activate_session(session, goptions) + + tag = session.getTag(args[0]) + if not tag: + parser.error("No such tag: %s" % args[0]) + + parent = None + priority = None + if len(args) > 1: + parent = session.getTag(args[1]) + if not parent: + parser.error("No such tag: %s" % args[1]) + if len(args) > 2: + priority = args[2] + + data = session.getInheritanceData(tag['id']) + if parent and data: + data = [datum for datum in data if datum['parent_id'] == parent['id']] + if priority and data: + data = [datum for datum in data if datum['priority'] == priority] + + if len(data) == 0: + error("No inheritance link found to remove. Please check your arguments") + elif len(data) > 1: + print("Multiple matches for tag.") + if not parent: + error("Please specify a parent on the command line.") + if not priority: + error("Please specify a priority on the command line.") + error("Error: Key constraints may be broken. Exiting.") + + # len(data) == 1 + data = data[0] + + inheritanceData = session.getInheritanceData(tag['id']) + samePriority = [datum for datum in inheritanceData if datum['priority'] == options.priority] + if samePriority: + error("Error: There is already an active inheritance with that priority on %s, " + "please specify a different priority with --priority." % tag['name']) + + new_data = data.copy() + if options.priority is not None and options.priority.isdigit(): + new_data['priority'] = int(options.priority) + if options.maxdepth is not None: + if options.maxdepth.isdigit(): + new_data['maxdepth'] = int(options.maxdepth) + elif options.maxdepth.lower() == "none": + new_data['maxdepth'] = None + else: + error("Invalid maxdepth: %s" % options.maxdepth) + if options.intransitive: + new_data['intransitive'] = options.intransitive + if options.noconfig: + new_data['noconfig'] = options.noconfig + if options.pkg_filter: + new_data['pkg_filter'] = options.pkg_filter + + # find the data we want to edit and replace it + index = inheritanceData.index(data) + inheritanceData[index] = new_data + session.setInheritanceData(tag['id'], inheritanceData) + + diff --git a/cli/koji_cli/commands/cmd_edit_target.py b/cli/koji_cli/commands/cmd_edit_target.py new file mode 100644 index 0000000..089dcd4 --- /dev/null +++ b/cli/koji_cli/commands/cmd_edit_target.py @@ -0,0 +1,56 @@ +from __future__ import absolute_import, division + +from optparse import OptionParser + + +import koji + +from koji_cli.lib import ( + activate_session, + error, + get_usage_str +) + + +def handle_edit_target(goptions, session, args): + "[admin] Set the name, build_tag, and/or dest_tag of an existing build target to new values" + usage = "usage: %prog edit-target [options] " + parser = OptionParser(usage=get_usage_str(usage)) + parser.add_option("--rename", help="Specify new name for target") + parser.add_option("--build-tag", help="Specify a different build tag") + parser.add_option("--dest-tag", help="Specify a different destination tag") + + (options, args) = parser.parse_args(args) + + if len(args) != 1: + parser.error("Please specify a build target") + activate_session(session, goptions) + + if not (session.hasPerm('admin') or session.hasPerm('target')): + parser.error("This action requires target or admin privileges") + + targetInfo = session.getBuildTarget(args[0]) + if targetInfo is None: + raise koji.GenericError("No such build target: %s" % args[0]) + + targetInfo['orig_name'] = targetInfo['name'] + + if options.rename: + targetInfo['name'] = options.rename + if options.build_tag: + targetInfo['build_tag_name'] = options.build_tag + chkbuildtag = session.getTag(options.build_tag) + if not chkbuildtag: + error("No such tag: %s" % options.build_tag) + if not chkbuildtag.get("arches", None): + error("Build tag has no arches: %s" % options.build_tag) + if options.dest_tag: + chkdesttag = session.getTag(options.dest_tag) + if not chkdesttag: + error("No such destination tag: %s" % options.dest_tag) + targetInfo['dest_tag_name'] = options.dest_tag + + session.editBuildTarget(targetInfo['orig_name'], targetInfo['name'], + targetInfo['build_tag_name'], targetInfo['dest_tag_name']) + + diff --git a/cli/koji_cli/commands/cmd_edit_user.py b/cli/koji_cli/commands/cmd_edit_user.py new file mode 100644 index 0000000..6bdd166 --- /dev/null +++ b/cli/koji_cli/commands/cmd_edit_user.py @@ -0,0 +1,42 @@ +from __future__ import absolute_import, division + +from optparse import OptionParser + + + +from koji_cli.lib import ( + activate_session, + arg_filter, + get_usage_str +) + + +def handle_edit_user(goptions, session, args): + "[admin] Alter user information" + usage = "usage: %prog edit-user [options]" + parser = OptionParser(usage=get_usage_str(usage)) + parser.add_option("--rename", help="Rename the user") + parser.add_option("--edit-krb", action="append", default=[], metavar="OLD=NEW", + help="Change kerberos principal of the user") + parser.add_option("--add-krb", action="append", default=[], metavar="KRB", + help="Add kerberos principal of the user") + parser.add_option("--remove-krb", action="append", default=[], metavar="KRB", + help="Remove kerberos principal of the user") + (options, args) = parser.parse_args(args) + if len(args) < 1: + parser.error("You must specify the username of the user to edit") + elif len(args) > 1: + parser.error("This command only accepts one argument (username)") + activate_session(session, goptions) + user = args[0] + princ_mappings = [] + for p in options.edit_krb: + old, new = p.split('=', 1) + princ_mappings.append({'old': arg_filter(old), 'new': arg_filter(new)}) + for a in options.add_krb: + princ_mappings.append({'old': None, 'new': arg_filter(a)}) + for r in options.remove_krb: + princ_mappings.append({'old': arg_filter(r), 'new': None}) + session.editUser(user, options.rename, princ_mappings) + + diff --git a/cli/koji_cli/commands/cmd_enable_channel.py b/cli/koji_cli/commands/cmd_enable_channel.py new file mode 100644 index 0000000..ba5825c --- /dev/null +++ b/cli/koji_cli/commands/cmd_enable_channel.py @@ -0,0 +1,39 @@ +from __future__ import absolute_import, division + +from optparse import OptionParser + +from six.moves import zip + + +from koji_cli.lib import ( + activate_session, + error, + get_usage_str +) + + +def handle_enable_channel(goptions, session, args): + "[admin] Mark one or more channels as enabled" + usage = "usage: %prog enable-channel [options] [ ...]" + parser = OptionParser(usage=get_usage_str(usage)) + parser.add_option("--comment", help="Comment indicating why the channel(s) are being enabled") + (options, args) = parser.parse_args(args) + + if not args: + parser.error("At least one channel must be specified") + + activate_session(session, goptions) + with session.multicall() as m: + result = [m.getChannel(channel, strict=False) for channel in args] + error_hit = False + for channel, id in zip(args, result): + if not id.result: + print("No such channel: %s" % channel) + error_hit = True + if error_hit: + error("No changes made. Please correct the command line.") + + with session.multicall() as m: + [m.enableChannel(channel, comment=options.comment) for channel in args] + + diff --git a/cli/koji_cli/commands/cmd_enable_host.py b/cli/koji_cli/commands/cmd_enable_host.py new file mode 100644 index 0000000..b562364 --- /dev/null +++ b/cli/koji_cli/commands/cmd_enable_host.py @@ -0,0 +1,43 @@ +from __future__ import absolute_import, division + +from optparse import OptionParser + +from six.moves import zip + + +from koji_cli.lib import ( + activate_session, + error, + get_usage_str +) + + +def handle_enable_host(goptions, session, args): + "[admin] Mark one or more hosts as enabled" + usage = "usage: %prog enable-host [options] [ ...]" + parser = OptionParser(usage=get_usage_str(usage)) + parser.add_option("--comment", help="Comment indicating why the host(s) are being enabled") + (options, args) = parser.parse_args(args) + + if not args: + parser.error("At least one host must be specified") + + activate_session(session, goptions) + session.multicall = True + for host in args: + session.getHost(host) + error_hit = False + for host, [id] in zip(args, session.multiCall(strict=True)): + if not id: + print("No such host: %s" % host) + error_hit = True + if error_hit: + error("No changes made. Please correct the command line.") + session.multicall = True + for host in args: + session.enableHost(host) + if options.comment is not None: + session.editHost(host, comment=options.comment) + session.multiCall(strict=True) + + diff --git a/cli/koji_cli/commands/cmd_enable_user.py b/cli/koji_cli/commands/cmd_enable_user.py new file mode 100644 index 0000000..2e18d3e --- /dev/null +++ b/cli/koji_cli/commands/cmd_enable_user.py @@ -0,0 +1,26 @@ +from __future__ import absolute_import, division + +from optparse import OptionParser + + + +from koji_cli.lib import ( + activate_session, + get_usage_str +) + + +def handle_enable_user(goptions, session, args): + "[admin] Enable logins by a user" + usage = "usage: %prog enable-user " + parser = OptionParser(usage=get_usage_str(usage)) + (options, args) = parser.parse_args(args) + if len(args) < 1: + parser.error("You must specify the username of the user to enable") + elif len(args) > 1: + parser.error("This command only accepts one argument (username)") + username = args[0] + activate_session(session, goptions) + session.enableUser(username) + + diff --git a/cli/koji_cli/commands/cmd_free_task.py b/cli/koji_cli/commands/cmd_free_task.py new file mode 100644 index 0000000..9fc009d --- /dev/null +++ b/cli/koji_cli/commands/cmd_free_task.py @@ -0,0 +1,30 @@ +from __future__ import absolute_import, division + +from optparse import OptionParser + + + +from koji_cli.lib import ( + activate_session, + get_usage_str +) + + +def handle_free_task(goptions, session, args): + "[admin] Free a task" + usage = "usage: %prog free-task [options] [ ...]" + parser = OptionParser(usage=get_usage_str(usage)) + (options, args) = parser.parse_args(args) + activate_session(session, goptions) + tlist = [] + for task_id in args: + try: + tlist.append(int(task_id)) + except ValueError: + parser.error("task_id must be an integer") + if not tlist: + parser.error("please specify at least one task_id") + for task_id in tlist: + session.freeTask(task_id) + + diff --git a/cli/koji_cli/commands/cmd_grant_cg_access.py b/cli/koji_cli/commands/cmd_grant_cg_access.py new file mode 100644 index 0000000..366fe6b --- /dev/null +++ b/cli/koji_cli/commands/cmd_grant_cg_access.py @@ -0,0 +1,32 @@ +from __future__ import absolute_import, division + +from optparse import OptionParser + + + +from koji_cli.lib import ( + activate_session, + get_usage_str +) + + +def handle_grant_cg_access(goptions, session, args): + "[admin] Add a user to a content generator" + usage = "usage: %prog grant-cg-access " + parser = OptionParser(usage=get_usage_str(usage)) + parser.add_option("--new", action="store_true", help="Create a new content generator") + (options, args) = parser.parse_args(args) + if len(args) != 2: + parser.error("Please specify a user and content generator") + activate_session(session, goptions) + user = args[0] + cg = args[1] + uinfo = session.getUser(user) + if uinfo is None: + parser.error("No such user: %s" % user) + kwargs = {} + if options.new: + kwargs['create'] = True + session.grantCGAccess(uinfo['name'], cg, **kwargs) + + diff --git a/cli/koji_cli/commands/cmd_grant_permission.py b/cli/koji_cli/commands/cmd_grant_permission.py new file mode 100644 index 0000000..c72ee6c --- /dev/null +++ b/cli/koji_cli/commands/cmd_grant_permission.py @@ -0,0 +1,43 @@ +from __future__ import absolute_import, division + +from optparse import OptionParser + + + +from koji_cli.lib import ( + activate_session, + get_usage_str +) + + +def handle_grant_permission(goptions, session, args): + "[admin] Grant a permission to a user" + usage = "usage: %prog grant-permission [options] [ ...]" + parser = OptionParser(usage=get_usage_str(usage)) + parser.add_option("--new", action="store_true", + help="Create this permission if the permission does not exist") + parser.add_option("--description", + help="Add description about new permission") + (options, args) = parser.parse_args(args) + if len(args) < 2: + parser.error("Please specify a permission and at least one user") + activate_session(session, goptions) + perm = args[0] + names = args[1:] + users = [] + for n in names: + user = session.getUser(n) + if user is None: + parser.error("No such user: %s" % n) + users.append(user) + kwargs = {} + if options.new: + kwargs['create'] = True + if options.description: + kwargs['description'] = options.description + if options.description and not options.new: + parser.error("Option new must be specified with option description.") + for user in users: + session.grantPermission(user['name'], perm, **kwargs) + + diff --git a/cli/koji_cli/commands/cmd_hostinfo.py b/cli/koji_cli/commands/cmd_hostinfo.py new file mode 100644 index 0000000..eb0fd7c --- /dev/null +++ b/cli/koji_cli/commands/cmd_hostinfo.py @@ -0,0 +1,93 @@ +from __future__ import absolute_import, division + +from optparse import OptionParser + +from six.moves import range + +import koji + +from koji_cli.lib import ( + ensure_connection, + error, + get_usage_str, + warn +) + + +def anon_handle_hostinfo(goptions, session, args): + "[info] Print basic information about a host" + usage = "usage: %prog hostinfo [options] [ ...]" + parser = OptionParser(usage=get_usage_str(usage)) + (options, args) = parser.parse_args(args) + if len(args) < 1: + parser.error("Please specify a host") + ensure_connection(session, goptions) + error_hit = False + for host in args: + if host.isdigit(): + host = int(host) + info = session.getHost(host) + if info is None: + warn("No such host: %s\n" % host) + error_hit = True + continue + print("Name: %(name)s" % info) + print("ID: %(id)d" % info) + print("Arches: %(arches)s" % info) + print("Capacity: %(capacity)s" % info) + print("Task Load: %(task_load).2f" % info) + if info['description']: + description = info['description'].splitlines() + print("Description: %s" % description[0]) + for line in description[1:]: + print("%s%s" % (" " * 13, line)) + else: + print("Description:") + if info['comment']: + comment = info['comment'].splitlines() + print("Comment: %s" % comment[0]) + for line in comment[1:]: + print("%s%s" % (" " * 9, line)) + else: + print("Comment:") + print("Enabled: %s" % (info['enabled'] and 'yes' or 'no')) + print("Ready: %s" % (info['ready'] and 'yes' or 'no')) + try: + update = session.getLastHostUpdate(info['id'], ts=True) + except koji.ParameterError: + # Hubs prior to v1.25.0 do not have a "ts" parameter for getLastHostUpdate + update = session.getLastHostUpdate(info['id']) + if update is None: + update = "never" + else: + update = koji.formatTimeLong(update) + print("Last Update: %s" % update) + print("Channels: %s" % ' '.join([c['name'] + for c in session.listChannels(hostID=info['id'])])) + print("Active Buildroots:") + states = {0: "INIT", 1: "WAITING", 2: "BUILDING"} + rows = [('NAME', 'STATE', 'CREATION TIME')] + for s in range(0, 3): + for b in session.listBuildroots(hostID=info['id'], state=s): + rows.append((("%s-%s-%s" % (b['tag_name'], b['id'], b['repo_id'])), states[s], + b['create_event_time'][:b['create_event_time'].find('.')])) + if len(rows) > 1: + for row in rows: + print("%-50s %-10s %-20s" % row) + else: + print("None") + if error_hit: + error() + + +def _multicall_with_check(session, batch_size): + """Helper for running multicall inside handle_clone_tag""" + err = False + for r in session.multiCall(batch=batch_size): + if isinstance(r, dict): + warn(r['faultString']) + err = True + if err: + error('Errors during the last call. Target tag could be inconsistent.') + + diff --git a/cli/koji_cli/commands/cmd_image_build.py b/cli/koji_cli/commands/cmd_image_build.py new file mode 100644 index 0000000..1101b7c --- /dev/null +++ b/cli/koji_cli/commands/cmd_image_build.py @@ -0,0 +1,277 @@ +from __future__ import absolute_import, division + +import os +from optparse import OptionParser + + +import koji + +from koji_cli.lib import ( + _progress_callback, + _running_in_bg, + activate_session, + get_usage_str, + unique_path, + watch_tasks +) + + +def handle_image_build(options, session, args): + """[build] Create a disk image given an install tree""" + formats = ('vmdk', 'qcow', 'qcow2', 'vdi', 'vpc', 'rhevm-ova', + 'vsphere-ova', 'vagrant-virtualbox', 'vagrant-libvirt', + 'vagrant-vmware-fusion', 'vagrant-hyperv', 'docker', 'raw-xz', + 'liveimg-squashfs', 'tar-gz') + usage = "usage: %prog image-build [options] " \ + " [ ...]" + usage += "\n %prog image-build --config \n" + parser = OptionParser(usage=get_usage_str(usage)) + parser.add_option("--background", action="store_true", + help="Run the image creation task at a lower priority") + parser.add_option("--config", + help="Use a configuration file to define image-build options " + "instead of command line options (they will be ignored).") + parser.add_option("--disk-size", default=10, help="Set the disk device size in gigabytes") + parser.add_option("--distro", + help="specify the RPM based distribution the image will be based " + "on with the format RHEL-X.Y, CentOS-X.Y, SL-X.Y, or Fedora-NN. " + "The packages for the Distro you choose must have been built " + "in this system.") + parser.add_option("--format", default=[], action="append", + help="Convert results to one or more formats " + "(%s), this option may be used " + "multiple times. By default, specifying this option will " + "omit the raw disk image (which is 10G in size) from the " + "build results. If you really want it included with converted " + "images, pass in 'raw' as an option." % ', '.join(formats)) + parser.add_option("--kickstart", help="Path to a local kickstart file") + parser.add_option("--ksurl", metavar="SCMURL", + help="The URL to the SCM containing the kickstart file") + parser.add_option("--ksversion", metavar="VERSION", + help="The syntax version used in the kickstart file") + parser.add_option("--noprogress", action="store_true", + help="Do not display progress of the upload") + parser.add_option("--noverifyssl", action="store_true", + help="Use the noverifyssl option for the install tree and all repos. " + "This option is only allowed if enabled on the builder.") + parser.add_option("--nowait", action="store_false", dest="wait", + help="Don't wait on image creation") + parser.add_option("--ova-option", action="append", + help="Override a value in the OVA description XML. Provide a value " + "in a name=value format, such as 'ovf_memory_mb=6144'") + parser.add_option("--factory-parameter", nargs=2, action="append", + help="Pass a parameter to Image Factory. The results are highly specific " + "to the image format being created. This is a two argument parameter " + "that can be specified an arbitrary number of times. For example: " + "--factory-parameter docker_cmd '[ \"/bin/echo Hello World\" ]'") + parser.add_option("--release", help="Forcibly set the release field") + parser.add_option("--repo", action="append", + help="Specify a repo that will override the repo used to install " + "RPMs in the image. May be used multiple times. The " + "build tag repo associated with the target is the default.") + parser.add_option("--scratch", action="store_true", help="Create a scratch image") + parser.add_option("--skip-tag", action="store_true", help="Do not attempt to tag package") + parser.add_option("--can-fail", action="store", dest="optional_arches", + metavar="ARCH1,ARCH2,...", default="", + help="List of archs which are not blocking for build (separated by commas.") + parser.add_option("--specfile", metavar="URL", + help="SCM URL to spec file fragment to use to generate wrapper RPMs") + parser.add_option("--wait", action="store_true", + help="Wait on the image creation, even if running in the background") + + (task_options, args) = parser.parse_args(args) + + if task_options.config: + section = 'image-build' + config = koji.read_config_files([(task_options.config, True)]) + if not config.has_section(section): + parser.error("single section called [%s] is required" % section) + # pluck out the positional arguments first + args = [] + for arg in ('name', 'version', 'target', 'install_tree'): + args.append(config.get(section, arg)) + config.remove_option(section, arg) + args.extend(config.get(section, 'arches').split(',')) + config.remove_option(section, 'arches') + # turn comma-separated options into lists + for arg in ('repo', 'format'): + if config.has_option(section, arg): + setattr(task_options, arg, config.get(section, arg).split(',')) + config.remove_option(section, arg) + if config.has_option(section, 'can_fail'): + setattr(task_options, 'optional_arches', config.get(section, 'can_fail').split(',')) + config.remove_option(section, 'can_fail') + # handle everything else + for k, v in config.items(section): + setattr(task_options, k, v) + + # ova-options belong in their own section + section = 'ova-options' + if config.has_section(section): + task_options.ova_option = [] + for k, v in config.items(section): + task_options.ova_option.append('%s=%s' % (k, v)) + + # as do factory-parameters + section = 'factory-parameters' + if config.has_section(section): + task_options.factory_parameter = [] + for k, v in config.items(section): + # We do this, rather than a dict, to match what argparse spits out + task_options.factory_parameter.append((k, v)) + + else: + if len(args) < 5: + parser.error("At least five arguments are required: a name, a version, " + "a build target, a URL to an install tree, and 1 or more architectures.") + if not task_options.ksurl and not task_options.kickstart: + parser.error('You must specify --kickstart') + if not task_options.distro: + parser.error( + "You must specify --distro. Examples: Fedora-16, RHEL-6.4, SL-6.4 or CentOS-6.4") + return _build_image_oz(options, task_options, session, args) + + +def _build_image(options, task_opts, session, args, img_type): + """ + A private helper function that houses common CLI code for building + images with chroot-based tools. + """ + + if img_type not in ('livecd', 'appliance', 'livemedia'): + raise koji.GenericError('Unrecognized image type: %s' % img_type) + activate_session(session, options) + + # Set the task's priority. Users can only lower it with --background. + priority = None + if task_opts.background: + # relative to koji.PRIO_DEFAULT; higher means a "lower" priority. + priority = 5 + if _running_in_bg() or task_opts.noprogress: + callback = None + else: + callback = _progress_callback + + # We do some early sanity checking of the given target. + # Kojid gets these values again later on, but we check now as a convenience + # for the user. + target = args[2] + tmp_target = session.getBuildTarget(target) + if not tmp_target: + raise koji.GenericError("No such build target: %s" % target) + dest_tag = session.getTag(tmp_target['dest_tag']) + if not dest_tag: + raise koji.GenericError("No such destination tag: %s" % tmp_target['dest_tag_name']) + + # Set the architecture + if img_type == 'livemedia': + # livemedia accepts multiple arches + arch = [koji.canonArch(a) for a in args[3].split(",")] + else: + arch = koji.canonArch(args[3]) + + # Upload the KS file to the staging area. + # If it's a URL, it's kojid's job to go get it when it does the checkout. + ksfile = args[4] + + if not task_opts.ksurl: + serverdir = unique_path('cli-' + img_type) + session.uploadWrapper(ksfile, serverdir, callback=callback) + ksfile = os.path.join(serverdir, os.path.basename(ksfile)) + print('') + + hub_opts = {} + passthru_opts = [ + 'format', 'install_tree_url', 'isoname', 'ksurl', + 'ksversion', 'release', 'repo', 'scratch', 'skip_tag', + 'specfile', 'vcpu', 'vmem', 'volid', 'optional_arches', + 'lorax_dir', 'lorax_url', 'nomacboot', 'ksrepo', + 'squashfs_only', 'compress_arg', + ] + for opt in passthru_opts: + val = getattr(task_opts, opt, None) + if val is not None: + hub_opts[opt] = val + + if 'optional_arches' in hub_opts: + hub_opts['optional_arches'] = hub_opts['optional_arches'].split(',') + # finally, create the task. + task_id = session.buildImage(args[0], args[1], arch, target, ksfile, + img_type, opts=hub_opts, priority=priority) + + if not options.quiet: + print("Created task: %d" % task_id) + print("Task info: %s/taskinfo?taskID=%s" % (options.weburl, task_id)) + if task_opts.wait or (task_opts.wait is None and not _running_in_bg()): + session.logout() + return watch_tasks(session, [task_id], quiet=options.quiet, + poll_interval=options.poll_interval, topurl=options.topurl) + + +def _build_image_oz(options, task_opts, session, args): + """ + A private helper function that houses common CLI code for building + images with Oz and ImageFactory + """ + activate_session(session, options) + + # Set the task's priority. Users can only lower it with --background. + priority = None + if task_opts.background: + # relative to koji.PRIO_DEFAULT; higher means a "lower" priority. + priority = 5 + if _running_in_bg() or task_opts.noprogress: + callback = None + else: + callback = _progress_callback + + # We do some early sanity checking of the given target. + # Kojid gets these values again later on, but we check now as a convenience + # for the user. + target = args[2] + tmp_target = session.getBuildTarget(target) + if not tmp_target: + raise koji.GenericError("No such build target: %s" % target) + dest_tag = session.getTag(tmp_target['dest_tag']) + if not dest_tag: + raise koji.GenericError("No such destination tag: %s" % tmp_target['dest_tag_name']) + + # Set the architectures + arches = [] + for arch in args[4:]: + arches.append(koji.canonArch(arch)) + + # Upload the KS file to the staging area. + # If it's a URL, it's kojid's job to go get it when it does the checkout. + if not task_opts.ksurl: + if not task_opts.scratch: + # only scratch builds can omit ksurl + raise koji.GenericError("Non-scratch builds must provide ksurl") + ksfile = task_opts.kickstart + serverdir = unique_path('cli-image') + session.uploadWrapper(ksfile, serverdir, callback=callback) + task_opts.kickstart = os.path.join('work', serverdir, + os.path.basename(ksfile)) + print('') + + hub_opts = {} + for opt in ('ksurl', 'ksversion', 'kickstart', 'scratch', 'repo', + 'release', 'skip_tag', 'specfile', 'distro', 'format', + 'disk_size', 'ova_option', 'factory_parameter', + 'optional_arches', 'noverifyssl'): + val = getattr(task_opts, opt, None) + if val is not None: + hub_opts[opt] = val + # finally, create the task. + task_id = session.buildImageOz(args[0], args[1], arches, target, args[3], + opts=hub_opts, priority=priority) + + if not options.quiet: + print("Created task: %d" % task_id) + print("Task info: %s/taskinfo?taskID=%s" % (options.weburl, task_id)) + if task_opts.wait or (task_opts.wait is None and not _running_in_bg()): + session.logout() + return watch_tasks(session, [task_id], quiet=options.quiet, + poll_interval=options.poll_interval, topurl=options.topurl) + + diff --git a/cli/koji_cli/commands/cmd_image_build_indirection.py b/cli/koji_cli/commands/cmd_image_build_indirection.py new file mode 100644 index 0000000..1439386 --- /dev/null +++ b/cli/koji_cli/commands/cmd_image_build_indirection.py @@ -0,0 +1,151 @@ +from __future__ import absolute_import, division + +import os +from optparse import OptionParser + + +import koji + +from koji_cli.lib import ( + _progress_callback, + _running_in_bg, + activate_session, + get_usage_str, + unique_path, + watch_tasks +) + + +def handle_image_build_indirection(options, session, args): + """[build] Create a disk image using other disk images via the Indirection plugin""" + usage = "usage: %prog image-build-indirection [base_image] [utility_image] " \ + "[indirection_build_template]" + usage += "\n %prog image-build --config \n" + parser = OptionParser(usage=get_usage_str(usage)) + parser.add_option("--config", + help="Use a configuration file to define image-build options " + "instead of command line options (they will be ignored).") + parser.add_option("--background", action="store_true", + help="Run the image creation task at a lower priority") + parser.add_option("--name", help="Name of the output image") + parser.add_option("--version", help="Version of the output image") + parser.add_option("--release", help="Release of the output image") + parser.add_option("--arch", help="Architecture of the output image and input images") + parser.add_option("--target", help="Build target to use for the indirection build") + parser.add_option("--skip-tag", action="store_true", help="Do not tag the resulting build") + parser.add_option("--base-image-task", + help="ID of the createImage task of the base image to be used") + parser.add_option("--base-image-build", help="NVR or build ID of the base image to be used") + parser.add_option("--utility-image-task", + help="ID of the createImage task of the utility image to be used") + parser.add_option("--utility-image-build", + help="NVR or build ID of the utility image to be used") + parser.add_option("--indirection-template", + help="Name of the local file, or SCM file containing the template used to " + "drive the indirection plugin") + parser.add_option("--indirection-template-url", + help="SCM URL containing the template used to drive the indirection plugin") + parser.add_option("--results-loc", + help="Relative path inside the working space image where the results " + "should be extracted from") + parser.add_option("--scratch", action="store_true", help="Create a scratch image") + parser.add_option("--wait", action="store_true", + help="Wait on the image creation, even if running in the background") + parser.add_option("--nowait", action="store_false", dest="wait", + help="Do not wait on the image creation") + parser.add_option("--noprogress", action="store_true", + help="Do not display progress of the upload") + + (task_options, args) = parser.parse_args(args) + _build_image_indirection(options, task_options, session, args) + + +def _build_image_indirection(options, task_opts, session, args): + """ + A private helper function for builds using the indirection plugin of ImageFactory + """ + + # Do some sanity checks before even attempting to create the session + if not (bool(task_opts.utility_image_task) != + bool(task_opts.utility_image_build)): + raise koji.GenericError("You must specify either a utility-image task or build ID/NVR") + + if not (bool(task_opts.base_image_task) != + bool(task_opts.base_image_build)): + raise koji.GenericError("You must specify either a base-image task or build ID/NVR") + + required_opts = ['name', 'version', 'arch', 'target', 'indirection_template', 'results_loc'] + optional_opts = ['indirection_template_url', 'scratch', 'utility_image_task', + 'utility_image_build', 'base_image_task', 'base_image_build', 'release', + 'skip_tag'] + + missing = [] + for opt in required_opts: + if not getattr(task_opts, opt, None): + missing.append(opt) + + if len(missing) > 0: + print("Missing the following required options: %s" % + ' '.join(['--%s' % o.replace('_', '-') for o in missing])) + raise koji.GenericError("Missing required options specified above") + + activate_session(session, options) + + # Set the task's priority. Users can only lower it with --background. + priority = None + if task_opts.background: + # relative to koji.PRIO_DEFAULT; higher means a "lower" priority. + priority = 5 + if _running_in_bg() or task_opts.noprogress: + callback = None + else: + callback = _progress_callback + + # We do some early sanity checking of the given target. + # Kojid gets these values again later on, but we check now as a convenience + # for the user. + + tmp_target = session.getBuildTarget(task_opts.target) + if not tmp_target: + raise koji.GenericError("No such build target: %s" % tmp_target) + dest_tag = session.getTag(tmp_target['dest_tag']) + if not dest_tag: + raise koji.GenericError("No such destination tag: %s" % tmp_target['dest_tag_name']) + + # Set the architecture + task_opts.arch = koji.canonArch(task_opts.arch) + + # Upload the indirection template file to the staging area. + # If it's a URL, it's kojid's job to go get it when it does the checkout. + if not task_opts.indirection_template_url: + if not task_opts.scratch: + # only scratch builds can omit indirection_template_url + raise koji.GenericError( + "Non-scratch builds must provide a URL for the indirection template") + templatefile = task_opts.indirection_template + serverdir = unique_path('cli-image-indirection') + session.uploadWrapper(templatefile, serverdir, callback=callback) + task_opts.indirection_template = os.path.join('work', serverdir, + os.path.basename(templatefile)) + print('') + + hub_opts = {} + # Just pass everything in as opts. No posiitonal arguments at all. Why not? + for opt in required_opts + optional_opts: + val = getattr(task_opts, opt, None) + # We pass these through even if they are None + # The builder code can then check if they are set without using getattr + hub_opts[opt] = val + + # finally, create the task. + task_id = session.buildImageIndirection(opts=hub_opts, + priority=priority) + + if not options.quiet: + print("Created task: %d" % task_id) + print("Task info: %s/taskinfo?taskID=%s" % (options.weburl, task_id)) + if task_opts.wait or (task_opts.wait is None and not _running_in_bg()): + session.logout() + return watch_tasks(session, [task_id], quiet=options.quiet, topurl=options.topurl) + + diff --git a/cli/koji_cli/commands/cmd_import.py b/cli/koji_cli/commands/cmd_import.py new file mode 100644 index 0000000..cd46130 --- /dev/null +++ b/cli/koji_cli/commands/cmd_import.py @@ -0,0 +1,166 @@ +from __future__ import absolute_import, division + +import os +import sys +from optparse import OptionParser + + +import koji +from koji.util import to_list + +from koji_cli.lib import ( + activate_session, + get_usage_str, + linked_upload, + unique_path, + warn +) + + +def handle_import(goptions, session, args): + "[admin] Import externally built RPMs into the database" + usage = "usage: %prog import [options] [ ...]" + parser = OptionParser(usage=get_usage_str(usage)) + parser.add_option("--link", action="store_true", + help="Attempt to hardlink instead of uploading") + parser.add_option("--test", action="store_true", help="Don't actually import") + parser.add_option("--create-build", action="store_true", help="Auto-create builds as needed") + parser.add_option("--src-epoch", help="When auto-creating builds, use this epoch") + (options, args) = parser.parse_args(args) + if len(args) < 1: + parser.error("At least one package must be specified") + if options.src_epoch in ('None', 'none', '(none)'): + options.src_epoch = None + elif options.src_epoch: + try: + options.src_epoch = int(options.src_epoch) + except (ValueError, TypeError): + parser.error("Invalid value for epoch: %s" % options.src_epoch) + activate_session(session, goptions) + to_import = {} + for path in args: + data = koji.get_header_fields(path, ('name', 'version', 'release', 'epoch', + 'arch', 'sigmd5', 'sourcepackage', 'sourcerpm')) + if data['sourcepackage']: + data['arch'] = 'src' + nvr = "%(name)s-%(version)s-%(release)s" % data + else: + nvr = "%(name)s-%(version)s-%(release)s" % koji.parse_NVRA(data['sourcerpm']) + to_import.setdefault(nvr, []).append((path, data)) + builds_missing = False + nvrs = sorted(to_list(to_import.keys())) + for nvr in nvrs: + to_import[nvr].sort() + for path, data in to_import[nvr]: + if data['sourcepackage']: + break + else: + # no srpm included, check for build + binfo = session.getBuild(nvr) + if not binfo: + print("Missing build or srpm: %s" % nvr) + builds_missing = True + if builds_missing and not options.create_build: + print("Aborting import") + return + + # local function to help us out below + def do_import(path, data): + rinfo = dict([(k, data[k]) for k in ('name', 'version', 'release', 'arch')]) + prev = session.getRPM(rinfo) + if prev and not prev.get('external_repo_id', 0): + if prev['payloadhash'] == koji.hex_string(data['sigmd5']): + print("RPM already imported: %s" % path) + else: + warn("md5sum mismatch for %s" % path) + warn(" A different rpm with the same name has already been imported") + warn(" Existing sigmd5 is %r, your import has %r" % ( + prev['payloadhash'], koji.hex_string(data['sigmd5']))) + print("Skipping import") + return + if options.test: + print("Test mode -- skipping import for %s" % path) + return + serverdir = unique_path('cli-import') + if options.link: + linked_upload(path, serverdir) + else: + sys.stdout.write("uploading %s... " % path) + sys.stdout.flush() + session.uploadWrapper(path, serverdir) + print("done") + sys.stdout.flush() + sys.stdout.write("importing %s... " % path) + sys.stdout.flush() + try: + session.importRPM(serverdir, os.path.basename(path)) + except koji.GenericError as e: + print("\nError importing: %s" % str(e).splitlines()[-1]) + sys.stdout.flush() + else: + print("done") + sys.stdout.flush() + + for nvr in nvrs: + # check for existing build + need_build = True + binfo = session.getBuild(nvr) + if binfo: + b_state = koji.BUILD_STATES[binfo['state']] + if b_state == 'COMPLETE': + need_build = False + elif b_state in ['FAILED', 'CANCELED']: + if not options.create_build: + print("Build %s state is %s. Skipping import" % (nvr, b_state)) + continue + else: + print("Build %s exists with state=%s. Skipping import" % (nvr, b_state)) + continue + + # import srpms first, if any + for path, data in to_import[nvr]: + if data['sourcepackage']: + if binfo and b_state != 'COMPLETE': + # need to fix the state + print("Creating empty build: %s" % nvr) + b_data = koji.util.dslice(binfo, ['name', 'version', 'release']) + b_data['epoch'] = data['epoch'] + session.createEmptyBuild(**b_data) + binfo = session.getBuild(nvr) + do_import(path, data) + need_build = False + + if need_build: + # if we're doing this here, we weren't given the matching srpm + if not options.create_build: # pragma: no cover + if binfo: + # should have caught this earlier, but just in case... + b_state = koji.BUILD_STATES[binfo['state']] + print("Build %s state is %s. Skipping import" % (nvr, b_state)) + continue + else: + print("No such build: %s (include matching srpm or use " + "--create-build option to add it)" % nvr) + continue + else: + # let's make a new build + b_data = koji.parse_NVR(nvr) + if options.src_epoch: + b_data['epoch'] = options.src_epoch + else: + # pull epoch from first rpm + data = to_import[nvr][0][1] + b_data['epoch'] = data['epoch'] + if options.test: + print("Test mode -- would have created empty build: %s" % nvr) + else: + print("Creating empty build: %s" % nvr) + session.createEmptyBuild(**b_data) + binfo = session.getBuild(nvr) + + for path, data in to_import[nvr]: + if data['sourcepackage']: + continue + do_import(path, data) + + diff --git a/cli/koji_cli/commands/cmd_import_archive.py b/cli/koji_cli/commands/cmd_import_archive.py new file mode 100644 index 0000000..3c55a15 --- /dev/null +++ b/cli/koji_cli/commands/cmd_import_archive.py @@ -0,0 +1,118 @@ +from __future__ import absolute_import, division + +import os +from optparse import OptionParser + + +import koji + +from koji_cli.lib import ( + _progress_callback, + _running_in_bg, + activate_session, + arg_filter, + get_usage_str, + linked_upload, + unique_path +) + + +def handle_import_archive(options, session, args): + "[admin] Import an archive file and associate it with a build" + usage = "usage: %prog import-archive [ 1: + parser.error("You must specify a build ID or N-V-R and an archive to import") + + activate_session(session, options) + + if not suboptions.type: + parser.error("You must specify an archive type") + if suboptions.type == 'maven': + if not (session.hasPerm('maven-import') or session.hasPerm('admin')): + parser.error("This action requires the maven-import privilege") + if not suboptions.type_info: + parser.error("--type-info must point to a .pom file when importing Maven archives") + pom_info = koji.parse_pom(suboptions.type_info) + maven_info = koji.pom_to_maven_info(pom_info) + suboptions.type_info = maven_info + elif suboptions.type == 'win': + if not (session.hasPerm('win-import') or session.hasPerm('admin')): + parser.error("This action requires the win-import privilege") + if not suboptions.type_info: + parser.error("--type-info must be specified") + type_info = suboptions.type_info.split(':', 2) + if len(type_info) < 2: + parser.error("--type-info must be in relpath:platforms[:flags] format") + win_info = {'relpath': type_info[0], 'platforms': type_info[1].split()} + if len(type_info) > 2: + win_info['flags'] = type_info[2].split() + else: + win_info['flags'] = [] + suboptions.type_info = win_info + elif suboptions.type == 'image': + if not (session.hasPerm('image-import') or session.hasPerm('admin')): + parser.error("This action requires the image-import privilege") + if not suboptions.type_info: + parser.error("--type-info must be specified") + image_info = {'arch': suboptions.type_info} + suboptions.type_info = image_info + else: + parser.error("Unsupported archive type: %s" % suboptions.type) + + buildinfo = session.getBuild(arg_filter(args[0])) + if not buildinfo: + if not suboptions.create_build: + parser.error("No such build: %s" % args[0]) + buildinfo = koji.parse_NVR(args[0]) + if buildinfo['epoch'] == '': + buildinfo['epoch'] = None + else: + buildinfo['epoch'] = int(buildinfo['epoch']) + if suboptions.type == 'maven': + # --type-info should point to a local .pom file + session.createMavenBuild(buildinfo, suboptions.type_info) + elif suboptions.type == 'win': + # We're importing, so we don't know what platform the build + # was run on. Use "import" as a placeholder. + session.createWinBuild(buildinfo, {'platform': 'import'}) + elif suboptions.type == 'image': + # --type-info should have an arch of the image + session.createImageBuild(buildinfo) + else: + # should get caught above + assert False # pragma: no cover + + for filepath in args[1:]: + filename = os.path.basename(filepath) + print("Uploading archive: %s" % filename) + serverdir = unique_path('cli-import') + if _running_in_bg() or suboptions.noprogress: + callback = None + else: + callback = _progress_callback + if suboptions.link: + linked_upload(filepath, serverdir) + else: + session.uploadWrapper(filepath, serverdir, callback=callback) + print('') + serverpath = "%s/%s" % (serverdir, filename) + session.importArchive(serverpath, buildinfo, suboptions.type, suboptions.type_info) + print("Imported: %s" % filename) + + diff --git a/cli/koji_cli/commands/cmd_import_cg.py b/cli/koji_cli/commands/cmd_import_cg.py new file mode 100644 index 0000000..e7fbdd6 --- /dev/null +++ b/cli/koji_cli/commands/cmd_import_cg.py @@ -0,0 +1,71 @@ +from __future__ import absolute_import, division + +import os +from optparse import OptionParser + + +import koji + +from koji_cli.lib import ( + _progress_callback, + _running_in_bg, + activate_session, + error, + get_usage_str, + linked_upload, + unique_path +) + + +def handle_import_cg(goptions, session, args): + "[admin] Import external builds with rich metadata" + usage = "usage: %prog import-cg [options] " + parser = OptionParser(usage=get_usage_str(usage)) + parser.add_option("--noprogress", action="store_true", + help="Do not display progress of the upload") + parser.add_option("--link", action="store_true", + help="Attempt to hardlink instead of uploading") + parser.add_option("--test", action="store_true", help="Don't actually import") + parser.add_option("--token", action="store", default=None, help="Build reservation token") + (options, args) = parser.parse_args(args) + if len(args) < 2: + parser.error("Please specify metadata files directory") + activate_session(session, goptions) + metadata = koji.load_json(args[0]) + if 'output' not in metadata: + error("Metadata contains no output") + localdir = args[1] + + to_upload = [] + for info in metadata['output']: + if info.get('metadata_only', False): + continue + localpath = os.path.join(localdir, info.get('relpath', ''), info['filename']) + if not os.path.exists(localpath): + parser.error("No such file: %s" % localpath) + to_upload.append([localpath, info]) + + if options.test: + return + + # get upload path + # XXX - need a better way + serverdir = unique_path('cli-import') + + for localpath, info in to_upload: + relpath = os.path.join(serverdir, info.get('relpath', '')) + if _running_in_bg() or options.noprogress: + callback = None + else: + callback = _progress_callback + if options.link: + linked_upload(localpath, relpath) + else: + print("Uploading %s" % localpath) + session.uploadWrapper(localpath, relpath, callback=callback) + if callback: + print('') + + session.CGImport(metadata, serverdir, options.token) + + diff --git a/cli/koji_cli/commands/cmd_import_comps.py b/cli/koji_cli/commands/cmd_import_comps.py new file mode 100644 index 0000000..f5f1195 --- /dev/null +++ b/cli/koji_cli/commands/cmd_import_comps.py @@ -0,0 +1,106 @@ +from __future__ import absolute_import, division + +from optparse import OptionParser + +import six +import six.moves.xmlrpc_client + + +from koji_cli.lib import ( + activate_session, + error, + get_usage_str +) + + +def handle_import_comps(goptions, session, args): + "Import group/package information from a comps file" + usage = "usage: %prog import-comps [options] " + parser = OptionParser(usage=get_usage_str(usage)) + parser.add_option("--force", action="store_true", help="force import") + (local_options, args) = parser.parse_args(args) + if len(args) != 2: + parser.error("Incorrect number of arguments") + activate_session(session, goptions) + # check if the tag exists + dsttag = session.getTag(args[1]) + if dsttag is None: + error("No such tag: %s" % args[1]) + if libcomps is not None: + _import_comps(session, args[0], args[1], local_options) + elif yumcomps is not None: + _import_comps_alt(session, args[0], args[1], local_options) + else: + error("comps module not available") + + +def _import_comps(session, filename, tag, options): + """Import comps data using libcomps module""" + comps = libcomps.Comps() + comps.fromxml_f(filename) + force = options.force + ptypes = { + libcomps.PACKAGE_TYPE_DEFAULT: 'default', + libcomps.PACKAGE_TYPE_OPTIONAL: 'optional', + libcomps.PACKAGE_TYPE_CONDITIONAL: 'conditional', + libcomps.PACKAGE_TYPE_MANDATORY: 'mandatory', + libcomps.PACKAGE_TYPE_UNKNOWN: 'unknown', + } + for group in comps.groups: + print("Group: %s (%s)" % (group.id, group.name)) + session.groupListAdd( + tag, group.id, force=force, display_name=group.name, + is_default=bool(group.default), + uservisible=bool(group.uservisible), + description=group.desc, + langonly=group.lang_only, + biarchonly=bool(group.biarchonly)) + for pkg in group.packages: + pkgopts = {'type': ptypes[pkg.type], + 'basearchonly': bool(pkg.basearchonly), + } + if pkg.type == libcomps.PACKAGE_TYPE_CONDITIONAL: + pkgopts['requires'] = pkg.requires + for k in pkgopts.keys(): + if six.PY2 and isinstance(pkgopts[k], unicode): # noqa: F821 + pkgopts[k] = str(pkgopts[k]) + s_opts = ', '.join(["'%s': %r" % (k, pkgopts[k]) for k in sorted(pkgopts.keys())]) + print(" Package: %s: {%s}" % (pkg.name, s_opts)) + session.groupPackageListAdd(tag, group.id, pkg.name, force=force, **pkgopts) + # libcomps does not support group dependencies + # libcomps does not support metapkgs + + +def _import_comps_alt(session, filename, tag, options): # no cover 3.x + """Import comps data using yum.comps module""" + print('WARN: yum.comps does not support the biarchonly of group and basearchonly of package') + comps = yumcomps.Comps() + comps.add(filename) + force = options.force + for group in comps.groups: + print("Group: %(groupid)s (%(name)s)" % vars(group)) + session.groupListAdd(tag, group.groupid, force=force, display_name=group.name, + is_default=bool(group.default), + uservisible=bool(group.user_visible), + description=group.description, + langonly=group.langonly) + # yum.comps does not support the biarchonly field + for ptype, pdata in [('mandatory', group.mandatory_packages), + ('default', group.default_packages), + ('optional', group.optional_packages), + ('conditional', group.conditional_packages)]: + for pkg in pdata: + # yum.comps does not support basearchonly + pkgopts = {'type': ptype} + if ptype == 'conditional': + pkgopts['requires'] = pdata[pkg] + for k in pkgopts.keys(): + if six.PY2 and isinstance(pkgopts[k], unicode): # noqa: F821 + pkgopts[k] = str(pkgopts[k]) + s_opts = ', '.join(["'%s': %r" % (k, pkgopts[k]) for k in sorted(pkgopts.keys())]) + print(" Package: %s: {%s}" % (pkg, s_opts)) + session.groupPackageListAdd(tag, group.groupid, pkg, force=force, **pkgopts) + # yum.comps does not support group dependencies + # yum.comps does not support metapkgs + + diff --git a/cli/koji_cli/commands/cmd_import_sig.py b/cli/koji_cli/commands/cmd_import_sig.py new file mode 100644 index 0000000..61307b3 --- /dev/null +++ b/cli/koji_cli/commands/cmd_import_sig.py @@ -0,0 +1,84 @@ +from __future__ import absolute_import, division + +import os +from optparse import SUPPRESS_HELP, OptionParser + + +import koji +from koji.util import base64encode, md5_constructor + +from koji_cli.lib import ( + activate_session, + get_usage_str, + warn +) + + +def handle_import_sig(goptions, session, args): + "[admin] Import signatures into the database and write signed RPMs" + usage = "usage: %prog import-sig [options] [ ...]" + parser = OptionParser(usage=get_usage_str(usage)) + parser.add_option("--with-unsigned", action="store_true", + help="Also import unsigned sig headers") + parser.add_option("--write", action="store_true", help=SUPPRESS_HELP) + parser.add_option("--test", action="store_true", help="Test mode -- don't actually import") + (options, args) = parser.parse_args(args) + if len(args) < 1: + parser.error("At least one package must be specified") + for path in args: + if not os.path.exists(path): + parser.error("No such file: %s" % path) + activate_session(session, goptions) + for path in args: + data = koji.get_header_fields(path, ('name', 'version', 'release', 'arch', 'siggpg', + 'sigpgp', 'dsaheader', 'rsaheader', + 'sourcepackage')) + if data['sourcepackage']: + data['arch'] = 'src' + sigkey = data['siggpg'] + if not sigkey: + sigkey = data['sigpgp'] + if not sigkey: + sigkey = data['dsaheader'] + if not sigkey: + sigkey = data['rsaheader'] + if not sigkey: + sigkey = "" + if not options.with_unsigned: + print("Skipping unsigned package: %s" % path) + continue + else: + sigkey = koji.get_sigpacket_key_id(sigkey) + del data['siggpg'] + del data['sigpgp'] + del data['dsaheader'] + del data['rsaheader'] + rinfo = session.getRPM(data) + if not rinfo: + print("No such rpm in system: %(name)s-%(version)s-%(release)s.%(arch)s" % data) + continue + if rinfo.get('external_repo_id'): + print("Skipping external rpm: %(name)s-%(version)s-%(release)s.%(arch)s@" + "%(external_repo_name)s" % rinfo) + continue + sighdr = koji.rip_rpm_sighdr(path) + previous = session.queryRPMSigs(rpm_id=rinfo['id'], sigkey=sigkey) + assert len(previous) <= 1 + if previous: + sighash = md5_constructor(sighdr).hexdigest() + if previous[0]['sighash'] == sighash: + print("Signature already imported: %s" % path) + continue + else: + warn("signature mismatch: %s" % path) + warn(" The system already has a signature for this rpm with key %s" % sigkey) + warn(" The two signature headers are not the same") + continue + print("Importing signature [key %s] from %s..." % (sigkey, path)) + if not options.test: + session.addRPMSig(rinfo['id'], base64encode(sighdr)) + print("Writing signed copy") + if not options.test: + session.writeSignedRPM(rinfo['id'], sigkey) + + diff --git a/cli/koji_cli/commands/cmd_latest_build.py b/cli/koji_cli/commands/cmd_latest_build.py new file mode 100644 index 0000000..f3d1250 --- /dev/null +++ b/cli/koji_cli/commands/cmd_latest_build.py @@ -0,0 +1,104 @@ +from __future__ import absolute_import, division + +import os +import textwrap +from optparse import OptionParser + + +import koji + +from koji_cli.lib import ( + ensure_connection, + get_usage_str +) + + +def anon_handle_latest_build(goptions, session, args): + """[info] Print the latest builds for a tag""" + usage = """\ + usage: %prog latest-build [options] [ ...] + + The first option should be the name of a tag, not the name of a build target. + If you want to know the latest build in buildroots for a given build target, + then you should use the name of the build tag for that target. You can find + this value by running '%prog list-targets --name=' + + More information on tags and build targets can be found in the documentation. + https://docs.pagure.org/koji/HOWTO/#package-organization""" + + usage = textwrap.dedent(usage) + parser = OptionParser(usage=get_usage_str(usage)) + parser.add_option("--arch", help="List all of the latest packages for this arch") + parser.add_option("--all", action="store_true", + help="List all of the latest packages for this tag") + parser.add_option("--quiet", action="store_true", default=goptions.quiet, + help="Do not print the header information") + parser.add_option("--paths", action="store_true", help="Show the file paths") + parser.add_option("--type", + help="Show builds of the given type only. " + "Currently supported types: maven, win, image, or any custom " + "content generator btypes") + (options, args) = parser.parse_args(args) + if len(args) == 0: + parser.error("A tag name must be specified") + ensure_connection(session, goptions) + if options.all: + if len(args) > 1: + parser.error("A package name may not be combined with --all") + # Set None as the package argument + args.append(None) + else: + if len(args) < 2: + parser.error("A tag name and package name must be specified") + pathinfo = koji.PathInfo() + + for pkg in args[1:]: + if options.arch: + rpms, builds = session.getLatestRPMS(args[0], package=pkg, arch=options.arch) + builds_hash = dict([(x['build_id'], x) for x in builds]) + data = rpms + if options.paths: + for x in data: + z = x.copy() + x['name'] = builds_hash[x['build_id']]['package_name'] + x['path'] = os.path.join(pathinfo.build(x), pathinfo.rpm(z)) + fmt = "%(path)s" + else: + fmt = "%(name)s-%(version)s-%(release)s.%(arch)s" + else: + kwargs = {'package': pkg} + if options.type: + kwargs['type'] = options.type + data = session.getLatestBuilds(args[0], **kwargs) + if options.paths: + if options.type == 'maven': + for x in data: + x['path'] = pathinfo.mavenbuild(x) + fmt = "%(path)-40s %(tag_name)-20s %(maven_group_id)-20s " \ + "%(maven_artifact_id)-20s %(owner_name)s" + else: + for x in data: + x['path'] = pathinfo.build(x) + fmt = "%(path)-40s %(tag_name)-20s %(owner_name)s" + else: + if options.type == 'maven': + fmt = "%(nvr)-40s %(tag_name)-20s %(maven_group_id)-20s " \ + "%(maven_artifact_id)-20s %(owner_name)s" + else: + fmt = "%(nvr)-40s %(tag_name)-20s %(owner_name)s" + if not options.quiet: + if options.type == 'maven': + print("%-40s %-20s %-20s %-20s %s" % + ("Build", "Tag", "Group Id", "Artifact Id", "Built by")) + print("%s %s %s %s %s" % + ("-" * 40, "-" * 20, "-" * 20, "-" * 20, "-" * 16)) + else: + print("%-40s %-20s %s" % ("Build", "Tag", "Built by")) + print("%s %s %s" % ("-" * 40, "-" * 20, "-" * 16)) + options.quiet = True + + output = sorted([fmt % x for x in data]) + for line in output: + print(line) + + diff --git a/cli/koji_cli/commands/cmd_list_api.py b/cli/koji_cli/commands/cmd_list_api.py new file mode 100644 index 0000000..46a40d3 --- /dev/null +++ b/cli/koji_cli/commands/cmd_list_api.py @@ -0,0 +1,44 @@ +from __future__ import absolute_import, division + +from optparse import OptionParser + + + +from koji_cli.lib import ( + ensure_connection, + get_usage_str +) + + +def anon_handle_list_api(goptions, session, args): + "[info] Print the list of XML-RPC APIs" + usage = "usage: %prog list-api [options] [method_name ...]" + parser = OptionParser(usage=get_usage_str(usage)) + (options, args) = parser.parse_args(args) + ensure_connection(session, goptions) + if args: + for method in args: + help = session.system.methodHelp(method) + if not help: + parser.error("Unknown method: %s" % method) + print(help) + else: + for x in sorted(session._listapi(), key=lambda x: x['name']): + if 'argdesc' in x: + args = x['argdesc'] + elif x['args']: + # older servers may not provide argdesc + expanded = [] + for arg in x['args']: + if isinstance(arg, str): + expanded.append(arg) + else: + expanded.append('%s=%s' % (arg[0], arg[1])) + args = "(%s)" % ", ".join(expanded) + else: + args = "()" + print('%s%s' % (x['name'], args)) + if x['doc']: + print(" description: %s" % x['doc']) + + diff --git a/cli/koji_cli/commands/cmd_list_buildroot.py b/cli/koji_cli/commands/cmd_list_buildroot.py new file mode 100644 index 0000000..6d4ad71 --- /dev/null +++ b/cli/koji_cli/commands/cmd_list_buildroot.py @@ -0,0 +1,42 @@ +from __future__ import absolute_import, division + +from optparse import SUPPRESS_HELP, OptionParser + + + +from koji_cli.lib import ( + ensure_connection, + get_usage_str +) + + +def anon_handle_list_buildroot(goptions, session, args): + "[info] List the rpms used in or built in a buildroot" + usage = "usage: %prog list-buildroot [options] " + parser = OptionParser(usage=get_usage_str(usage)) + parser.add_option("--paths", action="store_true", help=SUPPRESS_HELP) + parser.add_option("--built", action="store_true", help="Show the built rpms") + parser.add_option("--verbose", "-v", action="store_true", help="Show more information") + (options, args) = parser.parse_args(args) + if len(args) != 1: + parser.error("Incorrect number of arguments") + ensure_connection(session, goptions) + if options.paths: + parser.error("--paths option is deprecated and will be removed in 1.30") + buildrootID = int(args[0]) + opts = {} + if options.built: + opts['buildrootID'] = buildrootID + else: + opts['componentBuildrootID'] = buildrootID + data = session.listRPMs(**opts) + + fmt = "%(nvr)s.%(arch)s" + order = sorted([(fmt % x, x) for x in data]) + for nvra, rinfo in order: + if options.verbose and rinfo.get('is_update'): + print("%s [update]" % nvra) + else: + print(nvra) + + diff --git a/cli/koji_cli/commands/cmd_list_builds.py b/cli/koji_cli/commands/cmd_list_builds.py new file mode 100644 index 0000000..328c167 --- /dev/null +++ b/cli/koji_cli/commands/cmd_list_builds.py @@ -0,0 +1,145 @@ +from __future__ import absolute_import, division + +from optparse import OptionParser + + +import koji + +from koji_cli.lib import ( + TimeOption, + ensure_connection, + get_usage_str, + warn +) + + +def anon_handle_list_builds(goptions, session, args): + "[info] Print the build listing" + usage = "usage: %prog list-builds [options]" + parser = OptionParser(usage=get_usage_str(usage), option_class=TimeOption) + parser.add_option("--package", help="List builds for this package") + parser.add_option("--buildid", help="List specific build from ID or nvr") + parser.add_option("--before", type="time", + help="List builds built before this time, " + TimeOption.get_help()) + parser.add_option("--after", type="time", + help="List builds built after this time (same format as for --before") + parser.add_option("--state", help="List builds in this state") + parser.add_option("--task", help="List builds for this task") + parser.add_option("--type", help="List builds of this type.") + parser.add_option("--prefix", help="Only builds starting with this prefix") + parser.add_option("--pattern", help="Only list builds matching this GLOB pattern") + parser.add_option("--cg", help="Only list builds imported by matching content generator name") + parser.add_option("--source", help="Only builds where the source field matches (glob pattern)") + parser.add_option("--owner", help="List builds built by this owner") + parser.add_option("--volume", help="List builds by volume ID") + parser.add_option("-k", "--sort-key", action="append", metavar='FIELD', + default=[], help="Sort the list by the named field. Allowed sort keys: " + "nvr, owner_name, state") + parser.add_option("-r", "--reverse", action="store_true", default=False, + help="Print the list in reverse order") + parser.add_option("--quiet", action="store_true", default=goptions.quiet, + help="Do not print the header information") + (options, args) = parser.parse_args(args) + if len(args) != 0: + parser.error("This command takes no arguments") + ensure_connection(session, goptions) + opts = {} + for key in ('type', 'prefix', 'pattern'): + value = getattr(options, key) + if value is not None: + opts[key] = value + if options.cg: + opts['cgID'] = options.cg + if options.package: + try: + opts['packageID'] = int(options.package) + except ValueError: + package = session.getPackageID(options.package) + if package is None: + parser.error("No such package: %s" % options.package) + opts['packageID'] = package + if options.owner: + try: + opts['userID'] = int(options.owner) + except ValueError: + user = session.getUser(options.owner) + if user is None: + parser.error("No such user: %s" % options.owner) + opts['userID'] = user['id'] + if options.volume: + try: + opts['volumeID'] = int(options.volume) + except ValueError: + volumes = session.listVolumes() + volumeID = None + for volume in volumes: + if options.volume == volume['name']: + volumeID = volume['id'] + if volumeID is None: + parser.error("No such volume: %s" % options.volume) + opts['volumeID'] = volumeID + if options.state: + try: + state = int(options.state) + if state > 4 or state < 0: + parser.error("Invalid state: %s" % options.state) + opts['state'] = state + except ValueError: + try: + opts['state'] = koji.BUILD_STATES[options.state] + except KeyError: + parser.error("Invalid state: %s" % options.state) + if options.before: + opts['completeBefore'] = options.before + if options.after: + opts['completeAfter'] = options.after + if options.task: + try: + opts['taskID'] = int(options.task) + except ValueError: + parser.error("Task id must be an integer") + if options.source: + opts['source'] = options.source + if options.buildid: + try: + buildid = int(options.buildid) + except ValueError: + buildid = options.buildid + data = [session.getBuild(buildid)] + if data[0] is None: + parser.error("No such build: '%s'" % buildid) + else: + # Check filter exists + if any(opts): + try: + data = session.listBuilds(**opts) + except koji.ParameterError as e: + if e.args[0].endswith("'pattern'"): + parser.error("The hub doesn't support the 'pattern' argument, please try" + " filtering the result on your local instead.") + if e.args[0].endswith("'cgID'"): + parser.error("The hub doesn't support the 'cg' argument, please try" + " filtering the result on your local instead.") + else: + parser.error("Filter must be provided for list") + if not options.sort_key: + options.sort_key = ['nvr'] + else: + for s_key in options.sort_key: + if s_key not in ['nvr', 'owner_name', 'state']: + warn("Invalid sort_key: %s." % s_key) + + data = sorted(data, key=lambda b: [b.get(k) for k in options.sort_key], + reverse=options.reverse) + for build in data: + build['state'] = koji.BUILD_STATES[build['state']] + + fmt = "%(nvr)-55s %(owner_name)-16s %(state)s" + if not options.quiet: + print("%-55s %-16s %s" % ("Build", "Built by", "State")) + print("%s %s %s" % ("-" * 55, "-" * 16, "-" * 16)) + + for build in data: + print(fmt % build) + + diff --git a/cli/koji_cli/commands/cmd_list_channels.py b/cli/koji_cli/commands/cmd_list_channels.py new file mode 100644 index 0000000..1f8fac9 --- /dev/null +++ b/cli/koji_cli/commands/cmd_list_channels.py @@ -0,0 +1,95 @@ +from __future__ import absolute_import, division + +from optparse import OptionParser + +from six.moves import zip + +import koji + +from koji_cli.lib import ( + ensure_connection, + get_usage_str, + truncate_string +) + + +def anon_handle_list_channels(goptions, session, args): + "[info] Print channels listing" + usage = "usage: %prog list-channels [options]" + parser = OptionParser(usage=get_usage_str(usage)) + parser.add_option("--simple", action="store_true", default=False, + help="Print just list of channels without additional info") + parser.add_option("--quiet", action="store_true", default=goptions.quiet, + help="Do not print header information") + parser.add_option("--comment", action="store_true", help="Show comments") + parser.add_option("--description", action="store_true", help="Show descriptions") + parser.add_option("--enabled", action="store_true", help="Limit to enabled channels") + parser.add_option("--not-enabled", action="store_false", dest="enabled", + help="Limit to not enabled channels") + parser.add_option("--disabled", action="store_false", dest="enabled", + help="Alias for --not-enabled") + (options, args) = parser.parse_args(args) + ensure_connection(session, goptions) + opts = {} + if options.enabled is not None: + opts['enabled'] = options.enabled + try: + channels = sorted([x for x in session.listChannels(**opts)], key=lambda x: x['name']) + except koji.ParameterError: + channels = sorted([x for x in session.listChannels()], key=lambda x: x['name']) + if len(channels) > 0: + first_item = channels[0] + else: + first_item = {} + session.multicall = True + for channel in channels: + session.listHosts(channelID=channel['id']) + for channel, [hosts] in zip(channels, session.multiCall()): + channel['enabled_host'] = len([x for x in hosts if x['enabled']]) + channel['disabled'] = len(hosts) - channel['enabled_host'] + channel['ready'] = len([x for x in hosts if x['ready']]) + channel['capacity'] = sum([x['capacity'] for x in hosts]) + channel['load'] = sum([x['task_load'] for x in hosts]) + if 'comment' in first_item: + channel['comment'] = truncate_string(channel['comment']) + if 'description' in first_item: + channel['description'] = truncate_string(channel['description']) + if channel['capacity']: + channel['perc_load'] = channel['load'] / channel['capacity'] * 100.0 + else: + channel['perc_load'] = 0.0 + if 'enabled' in first_item: + if not channel['enabled']: + channel['name'] = channel['name'] + ' [disabled]' + if channels: + longest_channel = max([len(ch['name']) for ch in channels]) + else: + longest_channel = 8 + if options.simple: + if not options.quiet: + hdr = 'Channel' + print(hdr) + print(len(hdr) * '-') + for channel in channels: + print(channel['name']) + else: + if not options.quiet: + hdr = '{channame:<{longest_channel}}Enabled Ready Disbld Load Cap ' \ + 'Perc ' + hdr = hdr.format(longest_channel=longest_channel, channame='Channel') + if options.description and 'description' in first_item: + hdr += "Description".ljust(53) + if options.comment and 'comment' in first_item: + hdr += "Comment".ljust(53) + print(hdr) + print(len(hdr) * '-') + mask = "%%(name)-%ss %%(enabled_host)6d %%(ready)6d %%(disabled)6d %%(load)6d %%(" \ + "capacity)6d %%(perc_load)6d%%%%" % longest_channel + if options.description and 'description' in first_item: + mask += " %(description)-50s" + if options.comment and 'comment' in first_item: + mask += " %(comment)-50s" + for channel in channels: + print(mask % channel) + + diff --git a/cli/koji_cli/commands/cmd_list_external_repos.py b/cli/koji_cli/commands/cmd_list_external_repos.py new file mode 100644 index 0000000..d37f2bd --- /dev/null +++ b/cli/koji_cli/commands/cmd_list_external_repos.py @@ -0,0 +1,117 @@ +from __future__ import absolute_import, division + +import time +from optparse import OptionParser + + +import koji + +from koji_cli.lib import ( + ensure_connection, + get_usage_str +) + + +def anon_handle_list_external_repos(goptions, session, args): + "[info] List external repos" + usage = "usage: %prog list-external-repos [options]" + parser = OptionParser(usage=get_usage_str(usage)) + parser.add_option("--url", help="Select by url") + parser.add_option("--name", help="Select by name") + parser.add_option("--id", type="int", help="Select by id") + parser.add_option("--tag", help="Select by tag") + parser.add_option("--used", action='store_true', help="List which tags use the repo(s)") + parser.add_option("--inherit", action='store_true', + help="Follow tag inheritance when selecting by tag") + parser.add_option("--event", type='int', metavar="EVENT#", help="Query at event") + parser.add_option("--ts", type='int', metavar="TIMESTAMP", + help="Query at last event before timestamp") + parser.add_option("--repo", type='int', metavar="REPO#", + help="Query at event corresponding to (nonexternal) repo") + parser.add_option("--quiet", action="store_true", default=goptions.quiet, + help="Do not display the column headers") + (options, args) = parser.parse_args(args) + if len(args) > 0: + parser.error("This command takes no arguments") + ensure_connection(session, goptions) + opts = {} + event = koji.util.eventFromOpts(session, options) + if event: + opts['event'] = event['id'] + event['timestr'] = time.asctime(time.localtime(event['ts'])) + print("Querying at event %(id)i (%(timestr)s)" % event) + if options.tag: + format = "tag" + opts['tag_info'] = options.tag + opts['repo_info'] = options.id or options.name or None + if opts['repo_info']: + if options.inherit: + parser.error("Can't select by repo when using --inherit") + if options.inherit: + del opts['repo_info'] + data = session.getExternalRepoList(**opts) + format = "multitag" + else: + data = session.getTagExternalRepos(**opts) + elif options.used: + format = "multitag" + opts['repo_info'] = options.id or options.name or None + data = session.getTagExternalRepos(**opts) + else: + format = "basic" + opts['info'] = options.id or options.name or None + opts['url'] = options.url or None + data = session.listExternalRepos(**opts) + + # There are three different output formats + # 1) Listing just repo data (name, url) + # 2) Listing repo data for a tag (priority, name, url) + # 3) Listing repo data for multiple tags (tag, priority, name, url) + if format == "basic": + format = "%(name)-25s %(url)s" + header1 = "%-25s %s" % ("External repo name", "URL") + header2 = "%s %s" % ("-" * 25, "-" * 40) + elif format == "tag": + format = "%(priority)-3i %(external_repo_name)-25s %(merge_mode)-10s %(url)s" + header1 = "%-3s %-25s %-10s URL" % ("Pri", "External repo name", "Mode") + header2 = "%s %s %s %s" % ("-" * 3, "-" * 25, "-" * 10, "-" * 40) + elif format == "multitag": + format = "%(tag_name)-20s %(priority)-3i %(merge_mode)-10s %(external_repo_name)s" + header1 = "%-20s %-3s %-10s %s" % ("Tag", "Pri", "Mode", "External repo name") + header2 = "%s %s %s %s" % ("-" * 20, "-" * 3, "-" * 10, "-" * 25) + if not options.quiet: + print(header1) + print(header2) + for rinfo in data: + # older hubs do not support merge_mode + rinfo.setdefault('merge_mode', None) + print(format % rinfo) + + +def _pick_external_repo_priority(session, tag): + """pick priority after current ones, leaving space for later insertions""" + repolist = session.getTagExternalRepos(tag_info=tag) + # ordered by priority + if not repolist: + priority = 5 + else: + priority = (repolist[-1]['priority'] + 7) // 5 * 5 + # at least 3 higher than current max and a multiple of 5 + return priority + + +def _parse_tagpri(tagpri): + parts = tagpri.rsplit('::', 1) + tag = parts[0] + if len(parts) == 1: + return tag, None + elif parts[1] in ('auto', '-1'): + return tag, None + else: + try: + pri = int(parts[1]) + except ValueError: + raise koji.GenericError("Invalid priority: %s" % parts[1]) + return tag, pri + + diff --git a/cli/koji_cli/commands/cmd_list_groups.py b/cli/koji_cli/commands/cmd_list_groups.py new file mode 100644 index 0000000..dc660a5 --- /dev/null +++ b/cli/koji_cli/commands/cmd_list_groups.py @@ -0,0 +1,64 @@ +from __future__ import absolute_import, division + +import time +from optparse import OptionParser + + +import koji + +from koji_cli.lib import ( + ensure_connection, + get_usage_str +) + + +def anon_handle_list_groups(goptions, session, args): + "[info] Print the group listings" + usage = "usage: %prog list-groups [options] []" + parser = OptionParser(usage=get_usage_str(usage)) + parser.add_option("--event", type='int', metavar="EVENT#", help="query at event") + parser.add_option("--ts", type='int', metavar="TIMESTAMP", + help="query at last event before timestamp") + parser.add_option("--repo", type='int', metavar="REPO#", help="query at event for a repo") + parser.add_option("--show-blocked", action="store_true", dest="incl_blocked", + help="Show blocked packages and groups") + (options, args) = parser.parse_args(args) + if len(args) < 1 or len(args) > 2: + parser.error("Incorrect number of arguments") + opts = {} + if options.incl_blocked: + opts['incl_blocked'] = True + ensure_connection(session, goptions) + event = koji.util.eventFromOpts(session, options) + if event: + opts['event'] = event['id'] + event['timestr'] = time.asctime(time.localtime(event['ts'])) + print("Querying at event %(id)i (%(timestr)s)" % event) + tmp_list = sorted([(x['name'], x) for x in session.getTagGroups(args[0], **opts)]) + groups = [x[1] for x in tmp_list] + + tags_cache = {} + + def get_cached_tag(tag_id): + if tag_id not in tags_cache: + tag = session.getTag(tag_id, strict=False) + if tag is None: + tags_cache[tag_id] = tag_id + else: + tags_cache[tag_id] = tag['name'] + return tags_cache[tag_id] + + for group in groups: + if len(args) > 1 and group['name'] != args[1]: + continue + print("%s [%s]" % (group['name'], get_cached_tag(group['tag_id']))) + groups = sorted([(x['name'], x) for x in group['grouplist']]) + for x in [x[1] for x in groups]: + x['tag_name'] = get_cached_tag(x['tag_id']) + print_group_list_req_group(x) + pkgs = sorted([(x['package'], x) for x in group['packagelist']]) + for x in [x[1] for x in pkgs]: + x['tag_name'] = get_cached_tag(x['tag_id']) + print_group_list_req_package(x) + + diff --git a/cli/koji_cli/commands/cmd_list_history.py b/cli/koji_cli/commands/cmd_list_history.py new file mode 100644 index 0000000..10e4e3f --- /dev/null +++ b/cli/koji_cli/commands/cmd_list_history.py @@ -0,0 +1,412 @@ +from __future__ import absolute_import, division + +import logging +import os +import sys +import time +import traceback +from optparse import SUPPRESS_HELP, OptionParser + +from six.moves import zip + +import koji + +from koji_cli.lib import ( + TimeOption, + ensure_connection, + error, + get_usage_str, + list_task_output_all_volumes +) + + +def anon_handle_list_history(goptions, session, args): + "[info] Display historical data" + usage = "usage: %prog list-history [options]" + parser = OptionParser(usage=get_usage_str(usage), option_class=TimeOption) + # Don't use local debug option, this one stays here for backward compatibility + # https://pagure.io/koji/issue/2084 + parser.add_option("--debug", action="store_true", default=goptions.debug, help=SUPPRESS_HELP) + parser.add_option("--build", help="Only show data for a specific build") + parser.add_option("--package", help="Only show data for a specific package") + parser.add_option("--tag", help="Only show data for a specific tag") + parser.add_option("--editor", "--by", metavar="USER", + help="Only show entries modified by user") + parser.add_option("--user", help="Only show entries affecting a user") + parser.add_option("--permission", help="Only show entries relating to a given permission") + parser.add_option("--cg", help="Only show entries relating to a given content generator") + parser.add_option("--external-repo", "--erepo", + help="Only show entries relating to a given external repo") + parser.add_option("--build-target", "--target", + help="Only show entries relating to a given build target") + parser.add_option("--group", help="Only show entries relating to a given group") + parser.add_option("--host", help="Only show entries related to given host") + parser.add_option("--channel", help="Only show entries related to given channel") + parser.add_option("--xkey", help="Only show entries related to given tag extra key") + parser.add_option("--before", type="time", + help="Only show entries before this time, " + TimeOption.get_help()) + parser.add_option("--after", type="time", + help="Only show entries after timestamp (same format as for --before)") + parser.add_option("--before-event", metavar="EVENT_ID", type='int', + help="Only show entries before event") + parser.add_option("--after-event", metavar="EVENT_ID", type='int', + help="Only show entries after event") + parser.add_option("--watch", action="store_true", help="Monitor history data") + parser.add_option("--active", action='store_true', + help="Only show entries that are currently active") + parser.add_option("--revoked", action='store_false', dest='active', + help="Only show entries that are currently revoked") + parser.add_option("--context", action="store_true", help="Show related entries") + parser.add_option("-s", "--show", action="append", help="Show data from selected tables") + parser.add_option("-v", "--verbose", action="store_true", help="Show more detail") + parser.add_option("-e", "--events", action="store_true", help="Show event ids") + parser.add_option("--all", action="store_true", + help="Allows listing the entire global history") + parser.add_option("--utc", action="store_true", + help="Shows time in UTC timezone") + (options, args) = parser.parse_args(args) + if len(args) != 0: + parser.error("This command takes no arguments") + kwargs = {} + limited = False + for opt in ('package', 'tag', 'build', 'editor', 'user', 'permission', + 'cg', 'external_repo', 'build_target', 'group', 'before', + 'after', 'host', 'channel', 'xkey'): + val = getattr(options, opt) + if val: + kwargs[opt] = val + limited = True + if options.before_event: + kwargs['beforeEvent'] = options.before_event + if options.after_event: + kwargs['afterEvent'] = options.after_event + if options.active is not None: + kwargs['active'] = options.active + if options.host: + hostinfo = session.getHost(options.host) + if not hostinfo: + error("No such host: %s" % options.host) + if options.channel: + channelinfo = session.getChannel(options.channel) + if not channelinfo: + error("No such channel: %s" % options.channel) + if options.utc: + kwargs['utc'] = options.utc + tables = None + if options.show: + tables = [] + for arg in options.show: + tables.extend(arg.split(',')) + if not limited and not options.all: + parser.error("Please specify an option to limit the query") + + ensure_connection(session, goptions) + + if options.watch: + if not kwargs.get('afterEvent') and not kwargs.get('after'): + kwargs['afterEvent'] = session.getLastEvent()['id'] + + while True: + histdata = session.queryHistory(tables=tables, **kwargs) + timeline = [] + + def distinguish_match(x, name): + """determine if create or revoke event matched""" + if options.context: + return True + name = "_" + name + ret = True + for key in x: + if key.startswith(name): + ret = ret and x[key] + return ret + for table in histdata: + hist = histdata[table] + for x in hist: + if x['revoke_event'] is not None: + if distinguish_match(x, 'revoked'): + timeline.append((x['revoke_event'], table, 0, x.copy())) + # pprint.pprint(timeline[-1]) + if distinguish_match(x, 'created'): + timeline.append((x['create_event'], table, 1, x)) + timeline.sort(key=lambda entry: entry[:3]) + # group edits together + new_timeline = [] + last_event = None + edit_index = {} + for entry in timeline: + event_id, table, create, x = entry + if event_id != last_event: + edit_index = {} + last_event = event_id + key = tuple([x[k] for k in _table_keys[table]]) + prev = edit_index.get((table, event_id), {}).get(key) + if prev: + prev[-1].setdefault('.related', []).append(entry) + else: + edit_index.setdefault((table, event_id), {})[key] = entry + new_timeline.append(entry) + for entry in new_timeline: + if options.debug: + print("%r" % list(entry)) + _print_histline(entry, options=options) + if not options.watch: + break + else: + time.sleep(goptions.poll_interval) + # repeat query for later events + if last_event: + kwargs['afterEvent'] = last_event + + +def _handleMap(lines, data, prefix=''): + for key, val in data.items(): + if key != '__starstar': + lines.append(' %s%s: %s' % (prefix, key, val)) + + +def _handleOpts(lines, opts, prefix=''): + if opts: + lines.append("%sOptions:" % prefix) + _handleMap(lines, opts, prefix) + + +def _parseTaskParams(session, method, task_id, topdir): + try: + return _do_parseTaskParams(session, method, task_id, topdir) + except Exception: + logger = logging.getLogger("koji") + if logger.isEnabledFor(logging.DEBUG): + tb_str = ''.join(traceback.format_exception(*sys.exc_info())) + logger.debug(tb_str) + return ['Unable to parse task parameters'] + + +def _do_parseTaskParams(session, method, task_id, topdir): + """Parse the return of getTaskRequest()""" + params = session.getTaskRequest(task_id) + + lines = [] + + if method == 'buildSRPMFromCVS': + lines.append("CVS URL: %s" % params[0]) + elif method == 'buildSRPMFromSCM': + lines.append("SCM URL: %s" % params[0]) + elif method == 'buildArch': + lines.append("SRPM: %s/work/%s" % (topdir, params[0])) + lines.append("Build Tag: %s" % session.getTag(params[1])['name']) + lines.append("Build Arch: %s" % params[2]) + lines.append("SRPM Kept: %r" % params[3]) + if len(params) > 4: + _handleOpts(lines, params[4]) + elif method == 'tagBuild': + build = session.getBuild(params[1]) + lines.append("Destination Tag: %s" % session.getTag(params[0])['name']) + lines.append("Build: %s" % koji.buildLabel(build)) + elif method == 'buildNotification': + build = params[1] + buildTarget = params[2] + lines.append("Recipients: %s" % (", ".join(params[0]))) + lines.append("Build: %s" % koji.buildLabel(build)) + lines.append("Build Target: %s" % buildTarget['name']) + lines.append("Web URL: %s" % params[3]) + elif method == 'build': + lines.append("Source: %s" % params[0]) + lines.append("Build Target: %s" % params[1]) + if len(params) > 2: + _handleOpts(lines, params[2]) + elif method == 'maven': + lines.append("SCM URL: %s" % params[0]) + lines.append("Build Target: %s" % params[1]) + if len(params) > 2: + _handleOpts(lines, params[2]) + elif method == 'buildMaven': + lines.append("SCM URL: %s" % params[0]) + lines.append("Build Tag: %s" % params[1]['name']) + if len(params) > 2: + _handleOpts(lines, params[2]) + elif method == 'wrapperRPM': + lines.append("Spec File URL: %s" % params[0]) + lines.append("Build Tag: %s" % params[1]['name']) + if params[2]: + lines.append("Build: %s" % koji.buildLabel(params[2])) + if params[3]: + lines.append("Task: %s %s" % (params[3]['id'], koji.taskLabel(params[3]))) + if len(params) > 4: + _handleOpts(lines, params[4]) + elif method == 'chainmaven': + lines.append("Builds:") + for package, opts in params[0].items(): + lines.append(" " + package) + _handleMap(lines, opts, prefix=" ") + lines.append("Build Target: %s" % params[1]) + if len(params) > 2: + _handleOpts(lines, params[2]) + elif method == 'winbuild': + lines.append("VM: %s" % params[0]) + lines.append("SCM URL: %s" % params[1]) + lines.append("Build Target: %s" % params[2]) + if len(params) > 3: + _handleOpts(lines, params[3]) + elif method == 'vmExec': + lines.append("VM: %s" % params[0]) + lines.append("Exec Params:") + for info in params[1]: + if isinstance(info, dict): + _handleMap(lines, info, prefix=' ') + else: + lines.append(" %s" % info) + if len(params) > 2: + _handleOpts(lines, params[2]) + elif method in ('createLiveCD', 'createAppliance', 'createLiveMedia'): + argnames = ['Name', 'Version', 'Release', 'Arch', 'Target Info', 'Build Tag', 'Repo', + 'Kickstart File'] + for n, v in zip(argnames, params): + lines.append("%s: %s" % (n, v)) + if len(params) > 8: + _handleOpts(lines, params[8]) + elif method in ('appliance', 'livecd', 'livemedia'): + argnames = ['Name', 'Version', 'Arches', 'Target', 'Kickstart'] + for n, v in zip(argnames, params): + lines.append("%s: %s" % (n, v)) + if len(params) > 5: + _handleOpts(lines, params[5]) + elif method == 'newRepo': + tag = session.getTag(params[0]) + lines.append("Tag: %s" % tag['name']) + elif method == 'prepRepo': + lines.append("Tag: %s" % params[0]['name']) + elif method == 'createrepo': + lines.append("Repo ID: %i" % params[0]) + lines.append("Arch: %s" % params[1]) + oldrepo = params[2] + if oldrepo: + lines.append("Old Repo ID: %i" % oldrepo['id']) + lines.append("Old Repo Creation: %s" % koji.formatTimeLong(oldrepo['create_ts'])) + if len(params) > 3: + lines.append("External Repos: %s" % + ', '.join([ext['external_repo_name'] for ext in params[3]])) + elif method == 'tagNotification': + destTag = session.getTag(params[2]) + srcTag = None + if params[3]: + srcTag = session.getTag(params[3]) + build = session.getBuild(params[4]) + user = session.getUser(params[5]) + + lines.append("Recipients: %s" % ", ".join(params[0])) + lines.append("Successful?: %s" % (params[1] and 'yes' or 'no')) + lines.append("Tagged Into: %s" % destTag['name']) + if srcTag: + lines.append("Moved From: %s" % srcTag['name']) + lines.append("Build: %s" % koji.buildLabel(build)) + lines.append("Tagged By: %s" % user['name']) + lines.append("Ignore Success?: %s" % (params[6] and 'yes' or 'no')) + if params[7]: + lines.append("Failure Message: %s" % params[7]) + elif method == 'dependantTask': + lines.append("Dependant Tasks: %s" % ", ".join([str(depID) for depID in params[0]])) + lines.append("Subtasks:") + for subtask in params[1]: + lines.append(" Method: %s" % subtask[0]) + lines.append(" Parameters: %s" % + ", ".join([str(subparam) for subparam in subtask[1]])) + if len(subtask) > 2 and subtask[2]: + subopts = subtask[2] + _handleOpts(lines, subopts, prefix=' ') + lines.append("") + elif method == 'chainbuild': + lines.append("Build Groups:") + group_num = 0 + for group_list in params[0]: + group_num += 1 + lines.append(" %i: %s" % (group_num, ', '.join(group_list))) + lines.append("Build Target: %s" % params[1]) + if len(params) > 2: + _handleOpts(lines, params[2]) + elif method == 'waitrepo': + lines.append("Build Target: %s" % params[0]) + if params[1]: + lines.append("Newer Than: %s" % params[1]) + if params[2]: + lines.append("NVRs: %s" % ', '.join(params[2])) + + return lines + + +def _printTaskInfo(session, task_id, topdir, level=0, recurse=True, verbose=True): + """Recursive function to print information about a task + and its children.""" + + BUILDDIR = '/var/lib/mock' + indent = " " * 2 * level + + info = session.getTaskInfo(task_id) + + if info is None: + raise koji.GenericError("No such task: %d" % task_id) + + if info['host_id']: + host_info = session.getHost(info['host_id']) + else: + host_info = None + buildroot_infos = session.listBuildroots(taskID=task_id) + build_info = session.listBuilds(taskID=task_id) + + files = list_task_output_all_volumes(session, task_id) + logs = [] + output = [] + for filename in files: + if filename.endswith('.log'): + logs += [os.path.join(koji.pathinfo.work(volume=volume), + koji.pathinfo.taskrelpath(task_id), + filename) for volume in files[filename]] + else: + output += [os.path.join(koji.pathinfo.work(volume=volume), + koji.pathinfo.taskrelpath(task_id), + filename) for volume in files[filename]] + + owner = session.getUser(info['owner'])['name'] + + print("%sTask: %d" % (indent, task_id)) + print("%sType: %s" % (indent, info['method'])) + if verbose: + print("%sRequest Parameters:" % indent) + for line in _parseTaskParams(session, info['method'], task_id, topdir): + print("%s %s" % (indent, line)) + print("%sOwner: %s" % (indent, owner)) + print("%sState: %s" % (indent, koji.TASK_STATES[info['state']].lower())) + print("%sCreated: %s" % (indent, time.asctime(time.localtime(info['create_ts'])))) + if info.get('start_ts'): + print("%sStarted: %s" % (indent, time.asctime(time.localtime(info['start_ts'])))) + if info.get('completion_ts'): + print("%sFinished: %s" % (indent, time.asctime(time.localtime(info['completion_ts'])))) + if host_info: + print("%sHost: %s" % (indent, host_info['name'])) + if build_info: + print("%sBuild: %s (%d)" % (indent, build_info[0]['nvr'], build_info[0]['build_id'])) + if buildroot_infos: + print("%sBuildroots:" % indent) + for root in buildroot_infos: + print("%s %s/%s-%d-%d/" % + (indent, BUILDDIR, root['tag_name'], root['id'], root['repo_id'])) + if logs: + print("%sLog Files:" % indent) + for log_path in logs: + print("%s %s" % (indent, log_path)) + if output: + print("%sOutput:" % indent) + for file_path in output: + print("%s %s" % (indent, file_path)) + + # white space + print('') + + if recurse: + level += 1 + children = session.getTaskChildren(task_id, request=True) + children.sort(key=lambda x: x['id']) + for child in children: + _printTaskInfo(session, child['id'], topdir, level, verbose=verbose) + + diff --git a/cli/koji_cli/commands/cmd_list_hosts.py b/cli/koji_cli/commands/cmd_list_hosts.py new file mode 100644 index 0000000..d404431 --- /dev/null +++ b/cli/koji_cli/commands/cmd_list_hosts.py @@ -0,0 +1,132 @@ +from __future__ import absolute_import, division + +from optparse import OptionParser + +from six.moves import zip + +import koji + +from koji_cli.lib import ( + ensure_connection, + get_usage_str, + warn, + truncate_string +) + + +def anon_handle_list_hosts(goptions, session, args): + "[info] Print the host listing" + usage = "usage: %prog list-hosts [options]" + parser = OptionParser(usage=get_usage_str(usage)) + parser.add_option("--arch", action="append", default=[], help="Specify an architecture") + parser.add_option("--channel", help="Specify a channel") + parser.add_option("--ready", action="store_true", help="Limit to ready hosts") + parser.add_option("--not-ready", action="store_false", dest="ready", + help="Limit to not ready hosts") + parser.add_option("--enabled", action="store_true", help="Limit to enabled hosts") + parser.add_option("--not-enabled", action="store_false", dest="enabled", + help="Limit to not enabled hosts") + parser.add_option("--disabled", action="store_false", dest="enabled", + help="Alias for --not-enabled") + parser.add_option("--quiet", action="store_true", default=goptions.quiet, + help="Do not print header information") + parser.add_option("--show-channels", action="store_true", help="Show host's channels") + parser.add_option("--comment", action="store_true", help="Show comments") + parser.add_option("--description", action="store_true", help="Show descriptions") + (options, args) = parser.parse_args(args) + opts = {} + ensure_connection(session, goptions) + if options.arch: + opts['arches'] = options.arch + if options.channel: + channel = session.getChannel(options.channel) + if not channel: + parser.error('No such channel: %s' % options.channel) + opts['channelID'] = channel['id'] + if options.ready is not None: + opts['ready'] = options.ready + if options.enabled is not None: + opts['enabled'] = options.enabled + tmp_list = sorted([(x['name'], x) for x in session.listHosts(**opts)]) + hosts = [x[1] for x in tmp_list] + + if not hosts: + warn("No hosts found.") + return + + def yesno(x): + if x: + return 'Y' + else: + return 'N' + + try: + first = session.getLastHostUpdate(hosts[0]['id'], ts=True) + opts = {'ts': True} + except koji.ParameterError: + # Hubs prior to v1.25.0 do not have a "ts" parameter for getLastHostUpdate + first = session.getLastHostUpdate(hosts[0]['id']) + opts = {} + + # pull in the last update using multicall to speed it up a bit + with session.multicall() as m: + result = [m.getLastHostUpdate(host['id'], **opts) for host in hosts[1:]] + updateList = [first] + [x.result for x in result] + + for host, update in zip(hosts, updateList): + if update is None: + host['update'] = '-' + else: + host['update'] = koji.formatTimeLong(update) + host['enabled'] = yesno(host['enabled']) + host['ready'] = yesno(host['ready']) + host['arches'] = ','.join(host['arches'].split()) + host['description'] = truncate_string(host['description']) + host['comment'] = truncate_string(host['comment']) + + # pull hosts' channels + if options.show_channels: + with session.multicall() as m: + result = [m.listChannels(host['id']) for host in hosts] + first_line_channel = result[0].result[0] + for host, channels in zip(hosts, result): + list_channels = [] + for c in channels.result: + if 'enabled' in first_line_channel: + if c['enabled']: + list_channels.append(c['name']) + else: + list_channels.append('*' + c['name']) + else: + list_channels.append(c['name']) + host['channels'] = ','.join(sorted(list_channels)) + + if hosts: + longest_host = max([len(h['name']) for h in hosts]) + else: + longest_host = 8 + + if not options.quiet: + hdr = "{hostname:<{longest_host}} Enb Rdy Load/Cap Arches " \ + "Last Update " + hdr = hdr.format(longest_host=longest_host, hostname='Hostname') + if options.description: + hdr += "Description".ljust(51) + if options.comment: + hdr += "Comment".ljust(51) + if options.show_channels: + hdr += "Channels" + print(hdr) + print(len(hdr) * '-') + mask = "%%(name)-%ss %%(enabled)-3s %%(ready)-3s %%(task_load)4.1f/%%(capacity)-4.1f " \ + "%%(arches)-16s %%(update)-35s" % longest_host + if options.description: + mask += " %(description)-50s" + if options.comment: + mask += " %(comment)-50s" + if options.show_channels: + mask += " %(channels)s" + for host in hosts: + print(mask % host) + + diff --git a/cli/koji_cli/commands/cmd_list_notifications.py b/cli/koji_cli/commands/cmd_list_notifications.py new file mode 100644 index 0000000..0ae68ce --- /dev/null +++ b/cli/koji_cli/commands/cmd_list_notifications.py @@ -0,0 +1,86 @@ +from __future__ import absolute_import, division + +from optparse import OptionParser + + + +from koji_cli.lib import ( + activate_session, + ensure_connection, + error, + get_usage_str +) + + +def anon_handle_list_notifications(goptions, session, args): + "[monitor] List user's notifications and blocks" + usage = "usage: %prog list-notifications [options]" + parser = OptionParser(usage=get_usage_str(usage)) + parser.add_option("--mine", action="store_true", help="Just print your notifications") + parser.add_option("--user", help="Only notifications for this user") + (options, args) = parser.parse_args(args) + + if len(args) != 0: + parser.error("This command takes no arguments") + if not options.mine and not options.user: + parser.error("Use --user or --mine.") + + if options.user: + ensure_connection(session, goptions) + user = session.getUser(options.user) + if not user: + error("No such user: %s" % options.user) + user_id = user['id'] + else: + activate_session(session, goptions) + user_id = None + + mask = "%(id)6s %(tag)-25s %(package)-25s %(email)-20s %(success)-12s" + headers = {'id': 'ID', + 'tag': 'Tag', + 'package': 'Package', + 'email': 'E-mail', + 'success': 'Success-only'} + head = mask % headers + notifications = session.getBuildNotifications(user_id) + if notifications: + print('Notifications') + print(head) + print('-' * len(head)) + for notification in notifications: + if notification['tag_id']: + notification['tag'] = session.getTag(notification['tag_id'])['name'] + else: + notification['tag'] = '*' + if notification['package_id']: + notification['package'] = session.getPackage(notification['package_id'])['name'] + else: + notification['package'] = '*' + notification['success'] = ['no', 'yes'][notification['success_only']] + print(mask % notification) + else: + print('No notifications') + + print('') + + mask = "%(id)6s %(tag)-25s %(package)-25s" + head = mask % headers + blocks = session.getBuildNotificationBlocks(user_id) + if blocks: + print('Notification blocks') + print(head) + print('-' * len(head)) + for notification in blocks: + if notification['tag_id']: + notification['tag'] = session.getTag(notification['tag_id'])['name'] + else: + notification['tag'] = '*' + if notification['package_id']: + notification['package'] = session.getPackage(notification['package_id'])['name'] + else: + notification['package'] = '*' + print(mask % notification) + else: + print('No notification blocks') + + diff --git a/cli/koji_cli/commands/cmd_list_permissions.py b/cli/koji_cli/commands/cmd_list_permissions.py new file mode 100644 index 0000000..4e8b6b2 --- /dev/null +++ b/cli/koji_cli/commands/cmd_list_permissions.py @@ -0,0 +1,60 @@ +from __future__ import absolute_import, division + +from optparse import OptionParser + + + +from koji_cli.lib import ( + activate_session, + error, + get_usage_str +) + + +def handle_list_permissions(goptions, session, args): + "[info] List user permissions" + usage = "usage: %prog list-permissions [options]" + parser = OptionParser(usage=get_usage_str(usage)) + parser.add_option("--user", help="List permissions for the given user") + parser.add_option("--mine", action="store_true", help="List your permissions") + parser.add_option("--quiet", action="store_true", default=goptions.quiet, + help="Do not print the header information") + (options, args) = parser.parse_args(args) + if len(args) > 0: + parser.error("This command takes no arguments") + activate_session(session, goptions) + perms = [] + if options.user: + user = session.getUser(options.user) + if not user: + error("No such user: %s" % options.user) + for p in session.getUserPerms(user['id']): + perms.append({'name': p}) + elif options.mine: + for p in session.getPerms(): + perms.append({'name': p}) + else: + for p in session.getAllPerms(): + perms.append({'name': p['name'], 'description': p['description']}) + if perms: + longest_perm = max([len(perm['name']) for perm in perms]) + perms = sorted(perms, key=lambda x: x['name']) + else: + longest_perm = 8 + if longest_perm < len('Permission name '): + longest_perm = len('Permission name ') + if not options.quiet: + hdr = '{permname:<{longest_perm}}' + hdr = hdr.format(longest_perm=longest_perm, permname='Permission name') + if perms and perms[0].get('description'): + hdr += " Description".ljust(53) + print(hdr) + print(len(hdr) * '-') + for perm in perms: + line = '{permname:<{longest_perm}}' + line = line.format(longest_perm=longest_perm, permname=perm['name']) + if perm.get('description'): + line += " %s" % perm['description'] + print(line) + + diff --git a/cli/koji_cli/commands/cmd_list_pkgs.py b/cli/koji_cli/commands/cmd_list_pkgs.py new file mode 100644 index 0000000..4cb3820 --- /dev/null +++ b/cli/koji_cli/commands/cmd_list_pkgs.py @@ -0,0 +1,107 @@ +from __future__ import absolute_import, division + +import time +from optparse import OptionParser + + +import koji + +from koji_cli.lib import ( + ensure_connection, + error, + get_usage_str +) + + +def anon_handle_list_pkgs(goptions, session, args): + "[info] Print the package listing for tag or for owner" + usage = "usage: %prog list-pkgs [options]" + parser = OptionParser(usage=get_usage_str(usage)) + parser.add_option("--owner", help="Specify owner") + parser.add_option("--tag", help="Specify tag") + parser.add_option("--package", help="Specify package") + parser.add_option("--quiet", action="store_true", default=goptions.quiet, + help="Do not print header information") + parser.add_option("--noinherit", action="store_true", help="Don't follow inheritance") + parser.add_option("--show-blocked", action="store_true", help="Show blocked packages") + parser.add_option("--show-dups", action="store_true", help="Show superseded owners") + parser.add_option("--event", type='int', metavar="EVENT#", help="query at event") + parser.add_option("--ts", type='int', metavar="TIMESTAMP", + help="query at last event before timestamp") + parser.add_option("--repo", type='int', metavar="REPO#", help="query at event for a repo") + (options, args) = parser.parse_args(args) + if len(args) != 0: + parser.error("This command takes no arguments") + ensure_connection(session, goptions) + opts = {} + if options.owner: + user = session.getUser(options.owner) + if user is None: + parser.error("No such user: %s" % options.owner) + opts['userID'] = user['id'] + if options.tag: + tag = session.getTag(options.tag) + if tag is None: + parser.error("No such tag: %s" % options.tag) + opts['tagID'] = tag['id'] + if options.package: + opts['pkgID'] = options.package + allpkgs = False + if not opts: + # no limiting clauses were specified + allpkgs = True + opts['inherited'] = not options.noinherit + # hiding dups only makes sense if we're querying a tag + if options.tag: + opts['with_dups'] = options.show_dups + else: + opts['with_dups'] = True + event = koji.util.eventFromOpts(session, options) + if event: + opts['event'] = event['id'] + event['timestr'] = time.asctime(time.localtime(event['ts'])) + print("Querying at event %(id)i (%(timestr)s)" % event) + + if not opts.get('tagID') and not opts.get('userID') and \ + not opts.get('pkgID'): + if opts.get('event'): + parser.error("--event and --ts makes sense only with --tag," + " --owner or --package") + if options.show_blocked: + parser.error("--show-blocked makes sense only with --tag," + " --owner or --package") + if options.show_blocked: + opts['with_blocked'] = options.show_blocked + + try: + data = session.listPackages(**opts) + except koji.ParameterError: + del opts['with_blocked'] + data = session.listPackages(**opts) + + if not data: + error("(no matching packages)") + if not options.quiet: + if allpkgs: + print("Package") + print('-' * 23) + else: + print("%-23s %-23s %-16s %-15s" % ('Package', 'Tag', 'Extra Arches', 'Owner')) + print("%s %s %s %s" % ('-' * 23, '-' * 23, '-' * 16, '-' * 15)) + for pkg in data: + if allpkgs: + print(pkg['package_name']) + else: + if not options.show_blocked and pkg.get('blocked', False): + continue + if 'tag_id' in pkg: + if pkg['extra_arches'] is None: + pkg['extra_arches'] = "" + fmt = "%(package_name)-23s %(tag_name)-23s %(extra_arches)-16s %(owner_name)-15s" + if pkg.get('blocked', False): + fmt += " [BLOCKED]" + else: + fmt = "%(package_name)s" + print(fmt % pkg) + + diff --git a/cli/koji_cli/commands/cmd_list_signed.py b/cli/koji_cli/commands/cmd_list_signed.py new file mode 100644 index 0000000..1a5fc75 --- /dev/null +++ b/cli/koji_cli/commands/cmd_list_signed.py @@ -0,0 +1,106 @@ +from __future__ import absolute_import, division + +import os +from optparse import SUPPRESS_HELP, OptionParser + + +import koji + +from koji_cli.lib import ( + activate_session, + get_usage_str +) + + +def handle_list_signed(goptions, session, args): + "[admin] List signed copies of rpms" + usage = "usage: %prog list-signed [options]" + description = "You must have local access to Koji's topdir filesystem." + parser = OptionParser(usage=get_usage_str(usage), description=description) + # Don't use local debug option, this one stays here for backward compatibility + # https://pagure.io/koji/issue/2084 + parser.add_option("--debug", action="store_true", default=goptions.debug, help=SUPPRESS_HELP) + parser.add_option("--key", help="Only list RPMs signed with this key") + parser.add_option("--build", help="Only list RPMs from this build") + parser.add_option("--rpm", help="Only list signed copies for this RPM") + parser.add_option("--tag", help="Only list RPMs within this tag") + (options, args) = parser.parse_args(args) + if not options.build and not options.tag and not options.rpm: + parser.error("At least one from --build, --rpm, --tag needs to be specified.") + activate_session(session, goptions) + qopts = {} + build_idx = {} + rpm_idx = {} + if options.key: + qopts['sigkey'] = options.key + + sigs = [] + if options.rpm: + rpm_info = options.rpm + try: + rpm_info = int(rpm_info) + except ValueError: + pass + rinfo = session.getRPM(rpm_info, strict=True) + rpm_idx[rinfo['id']] = rinfo + if rinfo.get('external_repo_id'): + parser.error("External rpm: %(name)s-%(version)s-%(release)s.%(arch)s@" + "%(external_repo_name)s" % rinfo) + sigs += session.queryRPMSigs(rpm_id=rinfo['id'], **qopts) + if options.build: + build = options.build + try: + build = int(build) + except ValueError: + pass + binfo = session.getBuild(build, strict=True) + build_idx[binfo['id']] = binfo + rpms = session.listRPMs(buildID=binfo['id']) + for rinfo in rpms: + rpm_idx[rinfo['id']] = rinfo + sigs += session.queryRPMSigs(rpm_id=rinfo['id'], **qopts) + if options.tag: + tag = options.tag + try: + tag = int(tag) + except ValueError: + pass + rpms, builds = session.listTaggedRPMS(tag, inherit=False, latest=False) + tagged = {} + for binfo in builds: + build_idx.setdefault(binfo['id'], binfo) + results = [] + # use batched multicall as there could be potentially a lot of results + # so we don't exhaust server resources + with session.multicall(batch=5000) as m: + for rinfo in rpms: + rpm_idx.setdefault(rinfo['id'], rinfo) + tagged[rinfo['id']] = 1 + results.append(m.queryRPMSigs(rpm_id=rinfo['id']), **qopts) + sigs += [x.result[0] for x in results] + + # Now figure out which sig entries actually have live copies + for sig in sigs: + rpm_id = sig['rpm_id'] + sigkey = sig['sigkey'] + if options.tag: + if tagged.get(rpm_id) is None: + continue + rinfo = rpm_idx.get(rpm_id) + if not rinfo: + rinfo = session.getRPM(rpm_id) + rpm_idx[rinfo['id']] = rinfo + binfo = build_idx.get(rinfo['build_id']) + if not binfo: + binfo = session.getBuild(rinfo['build_id']) + build_idx[binfo['id']] = binfo + binfo['name'] = binfo['package_name'] + builddir = koji.pathinfo.build(binfo) + signedpath = "%s/%s" % (builddir, koji.pathinfo.signed(rinfo, sigkey)) + if not os.path.exists(signedpath): + if goptions.debug: + print("No copy: %s" % signedpath) + continue + print(signedpath) + + diff --git a/cli/koji_cli/commands/cmd_list_tag_inheritance.py b/cli/koji_cli/commands/cmd_list_tag_inheritance.py new file mode 100644 index 0000000..5bf291e --- /dev/null +++ b/cli/koji_cli/commands/cmd_list_tag_inheritance.py @@ -0,0 +1,68 @@ +from __future__ import absolute_import, division + +import sys +import textwrap +import time +from optparse import SUPPRESS_HELP, OptionParser + + +import koji + +from koji_cli.lib import ( + ensure_connection, + get_usage_str +) + + +def anon_handle_list_tag_inheritance(goptions, session, args): + "[info] Print the inheritance information for a tag" + usage = """\ + usage: %prog list-tag-inheritance [options] + + Prints tag inheritance with basic information about links. + Four flags could be seen in the output: + M - maxdepth - limits inheritance to n-levels + F - package filter (packages ignored for inheritance) + I - intransitive link - inheritance immediately stops here + N - noconfig - if tag is used in buildroot, its configuration values will not be used + + Exact values for maxdepth and package filter can be inquired by taginfo command. + """ + usage = textwrap.dedent(usage) + parser = OptionParser(usage=get_usage_str(usage)) + parser.add_option("--reverse", action="store_true", + help="Process tag's children instead of its parents") + parser.add_option("--stop", help=SUPPRESS_HELP) + parser.add_option("--jump", help=SUPPRESS_HELP) + parser.add_option("--event", type='int', metavar="EVENT#", help="query at event") + parser.add_option("--ts", type='int', metavar="TIMESTAMP", + help="query at last event before timestamp") + parser.add_option("--repo", type='int', metavar="REPO#", help="query at event for a repo") + (options, args) = parser.parse_args(args) + if len(args) != 1: + parser.error("This command takes exactly one argument: a tag name or ID") + for deprecated in ('stop', 'jump'): + if getattr(options, deprecated): + parser.error("--%s option has been removed in 1.26" % deprecated) + ensure_connection(session, goptions) + event = koji.util.eventFromOpts(session, options) + if event: + event['timestr'] = time.asctime(time.localtime(event['ts'])) + print("Querying at event %(id)i (%(timestr)s)" % event) + if event: + tag = session.getTag(args[0], event=event['id']) + else: + tag = session.getTag(args[0]) + if not tag: + parser.error("No such tag: %s" % args[0]) + + opts = {} + opts['reverse'] = options.reverse or False + if event: + opts['event'] = event['id'] + + sys.stdout.write(' %s (%i)\n' % (tag['name'], tag['id'])) + data = session.getFullInheritance(tag['id'], **opts) + _printInheritance(data, None, opts['reverse']) + + diff --git a/cli/koji_cli/commands/cmd_list_tagged.py b/cli/koji_cli/commands/cmd_list_tagged.py new file mode 100644 index 0000000..aa70f9c --- /dev/null +++ b/cli/koji_cli/commands/cmd_list_tagged.py @@ -0,0 +1,130 @@ +from __future__ import absolute_import, division + +import os +import time +from optparse import OptionParser + + +import koji + +from koji_cli.lib import ( + ensure_connection, + get_usage_str +) + + +def anon_handle_list_tagged(goptions, session, args): + "[info] List the builds or rpms in a tag" + usage = "usage: %prog list-tagged [options] []" + parser = OptionParser(usage=get_usage_str(usage)) + parser.add_option("--arch", action="append", default=[], help="List rpms for this arch") + parser.add_option("--rpms", action="store_true", help="Show rpms instead of builds") + parser.add_option("--inherit", action="store_true", help="Follow inheritance") + parser.add_option("--latest", action="store_true", help="Only show the latest builds/rpms") + parser.add_option("--latest-n", type='int', metavar="N", + help="Only show the latest N builds/rpms") + parser.add_option("--quiet", action="store_true", default=goptions.quiet, + help="Do not print the header information") + parser.add_option("--paths", action="store_true", help="Show the file paths") + parser.add_option("--sigs", action="store_true", help="Show signatures") + parser.add_option("--type", + help="Show builds of the given type only. " + "Currently supported types: maven, win, image") + parser.add_option("--event", type='int', metavar="EVENT#", help="query at event") + parser.add_option("--ts", type='int', metavar="TIMESTAMP", + help="query at last event before timestamp") + parser.add_option("--repo", type='int', metavar="REPO#", help="query at event for a repo") + (options, args) = parser.parse_args(args) + if len(args) == 0: + parser.error("A tag name must be specified") + elif len(args) > 2: + parser.error("Only one package name may be specified") + ensure_connection(session, goptions) + pathinfo = koji.PathInfo() + package = None + if len(args) > 1: + package = args[1] + tag = args[0] + opts = {} + for key in ('latest', 'inherit'): + opts[key] = getattr(options, key) + if options.latest_n is not None: + opts['latest'] = options.latest_n + if package: + opts['package'] = package + if options.arch: + options.rpms = True + opts['arch'] = options.arch + if options.sigs: + opts['rpmsigs'] = True + options.rpms = True + if options.type: + opts['type'] = options.type + event = koji.util.eventFromOpts(session, options) + event_id = None + if event: + opts['event'] = event['id'] + event_id = event['id'] + event['timestr'] = time.asctime(time.localtime(event['ts'])) + if not options.quiet: + print("Querying at event %(id)i (%(timestr)s)" % event) + + # check if tag exist(s|ed) + taginfo = session.getTag(tag, event=event_id) + if not taginfo: + parser.error("No such tag: %s" % tag) + + if options.rpms: + rpms, builds = session.listTaggedRPMS(tag, **opts) + data = rpms + if options.paths: + build_idx = dict([(b['id'], b) for b in builds]) + for rinfo in data: + build = build_idx[rinfo['build_id']] + builddir = pathinfo.build(build) + if options.sigs: + sigkey = rinfo['sigkey'] + signedpath = os.path.join(builddir, pathinfo.signed(rinfo, sigkey)) + if os.path.exists(signedpath): + rinfo['path'] = signedpath + else: + rinfo['path'] = os.path.join(builddir, pathinfo.rpm(rinfo)) + fmt = "%(path)s" + data = [x for x in data if 'path' in x] + else: + fmt = "%(name)s-%(version)s-%(release)s.%(arch)s" + if options.sigs: + fmt = "%(sigkey)s " + fmt + else: + data = session.listTagged(tag, **opts) + if options.paths: + if options.type == 'maven': + for x in data: + x['path'] = pathinfo.mavenbuild(x) + fmt = "%(path)-40s %(tag_name)-20s %(maven_group_id)-20s " \ + "%(maven_artifact_id)-20s %(owner_name)s" + else: + for x in data: + x['path'] = pathinfo.build(x) + fmt = "%(path)-40s %(tag_name)-20s %(owner_name)s" + else: + if options.type == 'maven': + fmt = "%(nvr)-40s %(tag_name)-20s %(maven_group_id)-20s " \ + "%(maven_artifact_id)-20s %(owner_name)s" + else: + fmt = "%(nvr)-40s %(tag_name)-20s %(owner_name)s" + if not options.quiet: + if options.type == 'maven': + print("%-40s %-20s %-20s %-20s %s" % + ("Build", "Tag", "Group Id", "Artifact Id", "Built by")) + print("%s %s %s %s %s" % + ("-" * 40, "-" * 20, "-" * 20, "-" * 20, "-" * 16)) + else: + print("%-40s %-20s %s" % ("Build", "Tag", "Built by")) + print("%s %s %s" % ("-" * 40, "-" * 20, "-" * 16)) + + output = sorted([fmt % x for x in data]) + for line in output: + print(line) + + diff --git a/cli/koji_cli/commands/cmd_list_tags.py b/cli/koji_cli/commands/cmd_list_tags.py new file mode 100644 index 0000000..d61a417 --- /dev/null +++ b/cli/koji_cli/commands/cmd_list_tags.py @@ -0,0 +1,328 @@ +from __future__ import absolute_import, division + +import itertools +import pprint +import sys +import time +from datetime import datetime +from dateutil.tz import tzutc +from optparse import OptionParser + + +import koji +from koji.util import to_list + +from koji_cli.lib import ( + ensure_connection, + get_usage_str, + warn +) + + +def anon_handle_list_tags(goptions, session, args): + "[info] Print the list of tags" + usage = "usage: %prog list-tags [options] [pattern]" + parser = OptionParser(usage=get_usage_str(usage)) + parser.add_option("--show-id", action="store_true", help="Show tag ids") + parser.add_option("--verbose", action="store_true", help="Show more information") + parser.add_option("--unlocked", action="store_true", help="Only show unlocked tags") + parser.add_option("--build", help="Show tags associated with a build") + parser.add_option("--package", help="Show tags associated with a package") + (options, patterns) = parser.parse_args(args) + ensure_connection(session, goptions) + + pkginfo = {} + buildinfo = {} + + if options.package: + pkginfo = session.getPackage(options.package) + if not pkginfo: + parser.error("No such package: %s" % options.package) + + if options.build: + buildinfo = session.getBuild(options.build) + if not buildinfo: + parser.error("No such build: %s" % options.build) + + if not patterns: + # list everything if no pattern is supplied + tags = session.listTags(build=buildinfo.get('id', None), + package=pkginfo.get('id', None)) + else: + # The hub may not support the pattern option. We try with that first + # and fall back to the old way. + fallback = False + try: + tags = [] + with session.multicall(strict=True) as m: + for pattern in patterns: + tags.append(m.listTags(build=buildinfo.get('id', None), + package=pkginfo.get('id', None), + pattern=pattern)) + tags = list(itertools.chain(*[t.result for t in tags])) + except koji.ParameterError: + fallback = True + if fallback: + # without the pattern option, we have to filter client side + tags = session.listTags(buildinfo.get('id', None), pkginfo.get('id', None)) + tags = [t for t in tags if koji.util.multi_fnmatch(t['name'], patterns)] + + tags.sort(key=lambda x: x['name']) + # if options.verbose: + # fmt = "%(name)s [%(id)i] %(perm)s %(locked)s %(arches)s" + if options.show_id: + fmt = "%(name)s [%(id)i]" + else: + fmt = "%(name)s" + for tag in tags: + if options.unlocked: + if tag['locked'] or tag['perm']: + continue + if not options.verbose: + print(fmt % tag) + else: + sys.stdout.write(fmt % tag) + if tag['locked']: + sys.stdout.write(' [LOCKED]') + if tag['perm']: + sys.stdout.write(' [%(perm)s perm required]' % tag) + print('') + + +def _print_histline(entry, **kwargs): + options = kwargs['options'] + event_id, table, create, x = entry + who = None + edit = x.get('.related') + if edit: + del x['.related'] + bad_edit = None + if len(edit) != 1: + bad_edit = "%i elements" % (len(edit) + 1) + other = edit[0] + # check edit for sanity + if create or not other[2]: + bad_edit = "out of order" + if event_id != other[0]: + bad_edit = "non-matching" + if bad_edit: + warn("Unusual edit at event %i in table %s (%s)" % (event_id, table, bad_edit)) + # we'll simply treat them as separate events + pprint.pprint(entry) + pprint.pprint(edit) + _print_histline(entry, **kwargs) + for data in edit: + _print_histline(entry, **kwargs) + return + if create: + ts = x['create_ts'] + if 'creator_name' in x: + who = "by %(creator_name)s" + else: + ts = x['revoke_ts'] + if 'revoker_name' in x: + who = "by %(revoker_name)s" + if table == 'tag_listing': + if edit: + fmt = "%(name)s-%(version)s-%(release)s re-tagged into %(tag.name)s" + elif create: + fmt = "%(name)s-%(version)s-%(release)s tagged into %(tag.name)s" + else: + fmt = "%(name)s-%(version)s-%(release)s untagged from %(tag.name)s" + elif table == 'user_perms': + if edit: + fmt = "permission %(permission.name)s re-granted to %(user.name)s" + elif create: + fmt = "permission %(permission.name)s granted to %(user.name)s" + else: + fmt = "permission %(permission.name)s revoked for %(user.name)s" + elif table == 'user_groups': + if edit: + fmt = "user %(user.name)s re-added to group %(group.name)s" + elif create: + fmt = "user %(user.name)s added to group %(group.name)s" + else: + fmt = "user %(user.name)s removed from group %(group.name)s" + elif table == 'cg_users': + if edit: + fmt = "user %(user.name)s re-added to content generator %(content_generator.name)s" + elif create: + fmt = "user %(user.name)s added to content generator %(content_generator.name)s" + else: + fmt = "user %(user.name)s removed from content generator %(content_generator.name)s" + elif table == 'tag_packages': + if edit: + fmt = "package list entry for %(package.name)s in %(tag.name)s updated" + elif create: + fmt = "package list entry created: %(package.name)s in %(tag.name)s" + else: + fmt = "package list entry revoked: %(package.name)s in %(tag.name)s" + elif table == 'tag_package_owners': + if edit: + fmt = "package owner changed for %(package.name)s in %(tag.name)s" + elif create: + fmt = "package owner %(owner.name)s set for %(package.name)s in %(tag.name)s" + else: + fmt = "package owner %(owner.name)s revoked for %(package.name)s in %(tag.name)s" + elif table == 'tag_inheritance': + if edit: + fmt = "inheritance line %(tag.name)s->%(parent.name)s updated" + elif create: + fmt = "inheritance line %(tag.name)s->%(parent.name)s added" + else: + fmt = "inheritance line %(tag.name)s->%(parent.name)s removed" + elif table == 'tag_config': + if edit: + fmt = "tag configuration for %(tag.name)s altered" + elif create: + fmt = "new tag: %(tag.name)s" + else: + fmt = "tag deleted: %(tag.name)s" + elif table == 'tag_extra': + if edit: + fmt = "tag option %(key)s for tag %(tag.name)s altered" + elif create: + fmt = "added tag option %(key)s for tag %(tag.name)s" + else: + fmt = "tag option %(key)s removed for %(tag.name)s" + elif table == 'host_config': + if edit: + fmt = "host configuration for %(host.name)s altered" + elif create: + fmt = "new host: %(host.name)s" + else: + fmt = "host deleted: %(host.name)s" + elif table == 'host_channels': + if create: + fmt = "host %(host.name)s added to channel %(channels.name)s" + else: + fmt = "host %(host.name)s removed from channel %(channels.name)s" + elif table == 'build_target_config': + if edit: + fmt = "build target configuration for %(build_target.name)s updated" + elif create: + fmt = "new build target: %(build_target.name)s" + else: + fmt = "build target deleted: %(build_target.name)s" + elif table == 'external_repo_config': + if edit: + fmt = "external repo configuration for %(external_repo.name)s altered" + elif create: + fmt = "new external repo: %(external_repo.name)s" + else: + fmt = "external repo deleted: %(external_repo.name)s" + elif table == 'tag_external_repos': + if edit: + fmt = "external repo entry for %(external_repo.name)s in tag %(tag.name)s updated" + elif create: + fmt = "external repo entry for %(external_repo.name)s added to tag %(tag.name)s" + else: + fmt = "external repo entry for %(external_repo.name)s removed from tag %(tag.name)s" + elif table == 'group_config': + if edit: + fmt = "group %(group.name)s configuration for tag %(tag.name)s updated" + elif create: + fmt = "group %(group.name)s added to tag %(tag.name)s" + else: + fmt = "group %(group.name)s removed from tag %(tag.name)s" + elif table == 'group_req_listing': + if edit: + fmt = "group dependency %(group.name)s->%(req.name)s updated in tag %(tag.name)s" + elif create: + fmt = "group dependency %(group.name)s->%(req.name)s added in tag %(tag.name)s" + else: + fmt = "group dependency %(group.name)s->%(req.name)s dropped from tag %(tag.name)s" + elif table == 'group_package_listing': + if edit: + fmt = "package entry %(package)s in group %(group.name)s, tag %(tag.name)s updated" + elif create: + fmt = "package %(package)s added to group %(group.name)s in tag %(tag.name)s" + else: + fmt = "package %(package)s removed from group %(group.name)s in tag %(tag.name)s" + else: + if edit: + fmt = "%s entry updated" % table + elif create: + fmt = "%s entry created" % table + else: + fmt = "%s entry revoked" % table + if options.utc: + time_str = time.asctime(datetime.fromtimestamp(ts, tzutc()).timetuple()) + else: + time_str = time.asctime(time.localtime(ts)) + + parts = [time_str, fmt % x] + if options.events or options.verbose: + parts.insert(1, "(eid %i)" % event_id) + if who: + parts.append(who % x) + if create and x['active']: + parts.append("[still active]") + print(' '.join(parts)) + hidden_fields = ['active', 'create_event', 'revoke_event', 'creator_id', 'revoker_id', + 'creator_name', 'revoker_name', 'create_ts', 'revoke_ts'] + + def get_nkey(key): + if key == 'perm_id': + return 'permission.name' + elif key.endswith('_id'): + return '%s.name' % key[:-3] + else: + return '%s.name' % key + if edit: + keys = sorted(to_list(x.keys())) + y = other[-1] + for key in keys: + if key in hidden_fields: + continue + if x[key] == y[key]: + continue + if key[0] == '_': + continue + nkey = get_nkey(key) + if nkey in x and nkey in y: + continue + print(" %s: %s -> %s" % (key, x[key], y[key])) + elif create and options.verbose and table != 'tag_listing': + keys = sorted(to_list(x.keys())) + # the table keys have already been represented in the base format string + also_hidden = list(_table_keys[table]) + also_hidden.extend([get_nkey(k) for k in also_hidden]) + for key in keys: + if key in hidden_fields or key in also_hidden: + continue + nkey = get_nkey(key) + if nkey in x: + continue + if key[0] == '_': + continue + if x.get('blocked') and key != 'blocked': + continue + if key.endswith('.name'): + dkey = key[:-5] + else: + dkey = key + print(" %s: %s" % (dkey, x[key])) + + +_table_keys = { + 'user_perms': ['user_id', 'perm_id'], + 'user_groups': ['user_id', 'group_id'], + 'cg_users': ['user_id', 'cg_id'], + 'tag_inheritance': ['tag_id', 'parent_id'], + 'tag_config': ['tag_id'], + 'tag_extra': ['tag_id', 'key'], + 'build_target_config': ['build_target_id'], + 'external_repo_config': ['external_repo_id'], + 'host_config': ['host_id'], + 'host_channels': ['host_id', 'channel_id'], + 'tag_external_repos': ['tag_id', 'external_repo_id'], + 'tag_listing': ['build_id', 'tag_id'], + 'tag_packages': ['package_id', 'tag_id'], + 'tag_package_owners': ['package_id', 'tag_id'], + 'group_config': ['group_id', 'tag_id'], + 'group_req_listing': ['group_id', 'tag_id', 'req_id'], + 'group_package_listing': ['group_id', 'tag_id', 'package'], +} + + diff --git a/cli/koji_cli/commands/cmd_list_targets.py b/cli/koji_cli/commands/cmd_list_targets.py new file mode 100644 index 0000000..296d8e8 --- /dev/null +++ b/cli/koji_cli/commands/cmd_list_targets.py @@ -0,0 +1,87 @@ +from __future__ import absolute_import, division + +import sys +from optparse import OptionParser + + + +from koji_cli.lib import ( + ensure_connection, + format_inheritance_flags, + get_usage_str +) + + +def anon_handle_list_targets(goptions, session, args): + "[info] List the build targets" + usage = "usage: %prog list-targets [options]" + parser = OptionParser(usage=get_usage_str(usage)) + parser.add_option("--name", help="Specify the build target name") + parser.add_option("--quiet", action="store_true", default=goptions.quiet, + help="Do not print the header information") + (options, args) = parser.parse_args(args) + + if len(args) != 0: + parser.error("This command takes no arguments") + ensure_connection(session, goptions) + + targets = session.getBuildTargets(options.name) + if len(targets) == 0: + if options.name: + parser.error('No such build target: %s' % options.name) + else: + parser.error('No targets were found') + + fmt = "%(name)-30s %(build_tag_name)-30s %(dest_tag_name)-30s" + if not options.quiet: + print("%-30s %-30s %-30s" % ('Name', 'Buildroot', 'Destination')) + print("-" * 93) + tmp_list = sorted([(x['name'], x) for x in targets]) + targets = [x[1] for x in tmp_list] + for target in targets: + print(fmt % target) + # pprint.pprint(session.getBuildTargets()) + + +def _printInheritance(tags, sibdepths=None, reverse=False): + if len(tags) == 0: + return + if sibdepths is None: + sibdepths = [] + currtag = tags[0] + tags = tags[1:] + if reverse: + siblings = len([tag for tag in tags if tag['parent_id'] == currtag['parent_id']]) + else: + siblings = len([tag for tag in tags if tag['child_id'] == currtag['child_id']]) + + sys.stdout.write(format_inheritance_flags(currtag)) + outdepth = 0 + for depth in sibdepths: + if depth < currtag['currdepth']: + outspacing = depth - outdepth + sys.stdout.write(' ' * (outspacing * 3 - 1)) + sys.stdout.write(_printable_unicode(u'\u2502')) + outdepth = depth + + sys.stdout.write(' ' * ((currtag['currdepth'] - outdepth) * 3 - 1)) + if siblings: + sys.stdout.write(_printable_unicode(u'\u251c')) + else: + sys.stdout.write(_printable_unicode(u'\u2514')) + sys.stdout.write(_printable_unicode(u'\u2500')) + if reverse: + sys.stdout.write('%(name)s (%(tag_id)i)\n' % currtag) + else: + sys.stdout.write('%(name)s (%(parent_id)i)\n' % currtag) + + if siblings: + if len(sibdepths) == 0 or sibdepths[-1] != currtag['currdepth']: + sibdepths.append(currtag['currdepth']) + else: + if len(sibdepths) > 0 and sibdepths[-1] == currtag['currdepth']: + sibdepths.pop() + + _printInheritance(tags, sibdepths, reverse) + + diff --git a/cli/koji_cli/commands/cmd_list_tasks.py b/cli/koji_cli/commands/cmd_list_tasks.py new file mode 100644 index 0000000..771318d --- /dev/null +++ b/cli/koji_cli/commands/cmd_list_tasks.py @@ -0,0 +1,55 @@ +from __future__ import absolute_import, division + +from optparse import OptionParser + + + +from koji_cli.lib import ( + TimeOption, + _list_tasks, + activate_session, + get_usage_str, + print_task_headers, + print_task_recurse +) + + +def handle_list_tasks(goptions, session, args): + "[info] Print the list of tasks" + usage = "usage: %prog list-tasks [options]" + parser = OptionParser(usage=get_usage_str(usage), option_class=TimeOption) + parser.add_option("--mine", action="store_true", help="Just print your tasks") + parser.add_option("--user", help="Only tasks for this user") + parser.add_option("--arch", help="Only tasks for this architecture") + parser.add_option("--method", help="Only tasks of this method") + parser.add_option("--channel", help="Only tasks in this channel") + parser.add_option("--host", help="Only tasks for this host") + parser.add_option("--quiet", action="store_true", default=goptions.quiet, + help="Do not display the column headers") + parser.add_option("--before", type="time", + help="List tasks completed before this time, " + TimeOption.get_help()) + parser.add_option("--after", type="time", + help="List tasks completed after this time (same format as for --before") + parser.add_option("--all", action="store_true", + help="List also finished tasks (valid only with --after)") + (options, args) = parser.parse_args(args) + if len(args) != 0: + parser.error("This command takes no arguments") + + if options.all and not options.after: + parser.error("--all must be used with --after") + + activate_session(session, goptions) + tasklist = _list_tasks(options, session) + if not tasklist: + print("(no tasks)") + return + if not options.quiet: + print_task_headers() + for t in tasklist: + if t.get('sub'): + # this subtask will appear under another task + continue + print_task_recurse(t) + + diff --git a/cli/koji_cli/commands/cmd_list_untagged.py b/cli/koji_cli/commands/cmd_list_untagged.py new file mode 100644 index 0000000..ef55921 --- /dev/null +++ b/cli/koji_cli/commands/cmd_list_untagged.py @@ -0,0 +1,77 @@ +from __future__ import absolute_import, division + +from optparse import OptionParser + + +import koji + +from koji_cli.lib import ( + ensure_connection, + error, + get_usage_str +) + + +def anon_handle_list_untagged(goptions, session, args): + "[info] List untagged builds" + usage = "usage: %prog list-untagged [options] []" + parser = OptionParser(usage=get_usage_str(usage)) + parser.add_option("--paths", action="store_true", help="Show the file paths") + parser.add_option("--show-references", action="store_true", help="Show build references") + (options, args) = parser.parse_args(args) + if len(args) > 1: + parser.error("Only one package name may be specified") + ensure_connection(session, goptions) + package = None + if len(args) > 0: + package = args[0] + opts = {} + if package: + package_id = session.getPackageID(package) + if package_id is None: + error("No such package: %s" % package) + opts['name'] = package + pathinfo = koji.PathInfo() + + data = session.untaggedBuilds(**opts) + if options.show_references: + print("(Showing build references)") + references = {} + with session.multicall(strict=True, batch=10000) as m: + for build in data: + references[build['id']] = m.buildReferences(build['id']) + + for build in data: + refs = references[build['id']].result + r = [] + if refs.get('rpms'): + r.append("rpms: %s" % refs['rpms']) + if refs.get('component_of'): + r.append("images/archives: %s" % refs['component_of']) + if refs.get('archives'): + r.append("archives buildroots: %s" % refs['archives']) + build['refs'] = ', '.join(r) + if options.paths: + for x in data: + x['path'] = pathinfo.build(x) + fmt = "%(path)s" + else: + fmt = "%(name)s-%(version)s-%(release)s" + if options.show_references: + fmt = fmt + " %(refs)s" + output = sorted([fmt % x for x in data]) + for line in output: + print(line) + + +def print_group_list_req_group(group): + print(" @%(name)s [%(tag_name)s]" % group) + + +def print_group_list_req_package(pkg): + fmt = " %(package)s: %(basearchonly)s, %(type)s [%(tag_name)s]" + if pkg['blocked']: + fmt += " [BLOCKED]" + print(fmt % pkg) + + diff --git a/cli/koji_cli/commands/cmd_list_volumes.py b/cli/koji_cli/commands/cmd_list_volumes.py new file mode 100644 index 0000000..03405bf --- /dev/null +++ b/cli/koji_cli/commands/cmd_list_volumes.py @@ -0,0 +1,22 @@ +from __future__ import absolute_import, division + +from optparse import OptionParser + + + +from koji_cli.lib import ( + ensure_connection, + get_usage_str +) + + +def anon_handle_list_volumes(goptions, session, args): + "[info] List storage volumes" + usage = "usage: %prog list-volumes" + parser = OptionParser(usage=get_usage_str(usage)) + (options, args) = parser.parse_args(args) + ensure_connection(session, goptions) + for volinfo in session.listVolumes(): + print(volinfo['name']) + + diff --git a/cli/koji_cli/commands/cmd_lock_tag.py b/cli/koji_cli/commands/cmd_lock_tag.py new file mode 100644 index 0000000..e871a99 --- /dev/null +++ b/cli/koji_cli/commands/cmd_lock_tag.py @@ -0,0 +1,62 @@ +from __future__ import absolute_import, division + +import fnmatch +from optparse import OptionParser + + + +from koji_cli.lib import ( + activate_session, + get_usage_str +) + + +def handle_lock_tag(goptions, session, args): + "[admin] Lock a tag" + usage = "usage: %prog lock-tag [options] [ ...]" + parser = OptionParser(usage=get_usage_str(usage)) + parser.add_option("--perm", help="Specify permission requirement") + parser.add_option("--glob", action="store_true", help="Treat args as glob patterns") + parser.add_option("--master", action="store_true", help="Lock the master lock") + parser.add_option("-n", "--test", action="store_true", help="Test mode") + (options, args) = parser.parse_args(args) + if len(args) < 1: + parser.error("Please specify a tag") + activate_session(session, goptions) + pdata = session.getAllPerms() + perm_ids = dict([(p['name'], p['id']) for p in pdata]) + perm = options.perm + if perm is None: + perm = 'admin' + perm_id = perm_ids[perm] + if options.glob: + selected = [] + for tag in session.listTags(): + for pattern in args: + if fnmatch.fnmatch(tag['name'], pattern): + selected.append(tag) + break + if not selected: + print("No tags matched") + else: + selected = [session.getTag(name, strict=True) for name in args] + for tag in selected: + if options.master: + # set the master lock + if tag['locked']: + print("Tag %s: master lock already set" % tag['name']) + continue + elif options.test: + print("Would have set master lock for: %s" % tag['name']) + continue + session.editTag2(tag['id'], locked=True) + else: + if tag['perm_id'] == perm_id: + print("Tag %s: %s permission already required" % (tag['name'], perm)) + continue + elif options.test: + print("Would have set permission requirement %s for tag %s" % (perm, tag['name'])) + continue + session.editTag2(tag['id'], perm_id=perm_id) + + diff --git a/cli/koji_cli/commands/cmd_make_task.py b/cli/koji_cli/commands/cmd_make_task.py new file mode 100644 index 0000000..0d27182 --- /dev/null +++ b/cli/koji_cli/commands/cmd_make_task.py @@ -0,0 +1,46 @@ +from __future__ import absolute_import, division + +from optparse import OptionParser + +from six.moves import map + + +from koji_cli.lib import ( + _running_in_bg, + activate_session, + arg_filter, + get_usage_str, + watch_tasks +) + + +def handle_make_task(goptions, session, args): + "[admin] Create an arbitrary task" + usage = "usage: %prog make-task [options] [ ...]" + parser = OptionParser(usage=get_usage_str(usage)) + parser.add_option("--channel", help="set channel") + parser.add_option("--priority", help="set priority") + parser.add_option("--watch", action="store_true", help="watch the task") + parser.add_option("--arch", help="set arch") + (options, args) = parser.parse_args(args) + if len(args) < 1: + parser.error("Please specify task method at least") + + activate_session(session, goptions) + taskopts = {} + for key in ('channel', 'priority', 'arch'): + value = getattr(options, key, None) + if value is not None: + taskopts[key] = value + task_id = session.makeTask(method=args[0], + arglist=list(map(arg_filter, args[1:])), + **taskopts) + print("Created task id %d" % task_id) + if _running_in_bg() or not options.watch: + return + else: + session.logout() + return watch_tasks(session, [task_id], quiet=goptions.quiet, + poll_interval=goptions.poll_interval, topurl=goptions.topurl) + + diff --git a/cli/koji_cli/commands/cmd_maven_build.py b/cli/koji_cli/commands/cmd_maven_build.py new file mode 100644 index 0000000..2b3183f --- /dev/null +++ b/cli/koji_cli/commands/cmd_maven_build.py @@ -0,0 +1,104 @@ +from __future__ import absolute_import, division + +from optparse import OptionParser + + +import koji +from koji.util import to_list + +from koji_cli.lib import ( + _running_in_bg, + activate_session, + get_usage_str, + watch_tasks +) + + +def handle_maven_build(options, session, args): + "[build] Build a Maven package from source" + usage = "usage: %prog maven-build [options] " + usage += "\n %prog maven-build --ini=CONFIG... [options] " + parser = OptionParser(usage=get_usage_str(usage)) + parser.add_option("--patches", action="store", metavar="URL", + help="SCM URL of a directory containing patches to apply to the sources " + "before building") + parser.add_option("-G", "--goal", action="append", dest="goals", metavar="GOAL", default=[], + help="Additional goal to run before \"deploy\"") + parser.add_option("-P", "--profile", action="append", dest="profiles", metavar="PROFILE", + default=[], help="Enable a profile for the Maven build") + parser.add_option("-D", "--property", action="append", dest="properties", metavar="NAME=VALUE", + default=[], help="Pass a system property to the Maven build") + parser.add_option("-E", "--env", action="append", dest="envs", metavar="NAME=VALUE", + default=[], help="Set an environment variable") + parser.add_option("-p", "--package", action="append", dest="packages", metavar="PACKAGE", + default=[], help="Install an additional package into the buildroot") + parser.add_option("-J", "--jvm-option", action="append", dest="jvm_options", metavar="OPTION", + default=[], help="Pass a command-line option to the JVM") + parser.add_option("-M", "--maven-option", action="append", dest="maven_options", + metavar="OPTION", default=[], help="Pass a command-line option to Maven") + parser.add_option("--ini", action="append", dest="inis", metavar="CONFIG", default=[], + help="Pass build parameters via a .ini file") + parser.add_option("-s", "--section", help="Get build parameters from this section of the .ini") + parser.add_option("--debug", action="store_true", help="Run Maven build in debug mode") + parser.add_option("--specfile", action="store", metavar="URL", + help="SCM URL of a spec file fragment to use to generate wrapper RPMs") + parser.add_option("--skip-tag", action="store_true", help="Do not attempt to tag package") + parser.add_option("--scratch", action="store_true", help="Perform a scratch build") + parser.add_option("--wait", action="store_true", + help="Wait on build, even if running in the background") + parser.add_option("--nowait", action="store_false", dest="wait", help="Don't wait on build") + parser.add_option("--quiet", action="store_true", + help="Do not print the task information", default=options.quiet) + parser.add_option("--background", action="store_true", + help="Run the build at a lower priority") + (build_opts, args) = parser.parse_args(args) + if build_opts.inis: + if len(args) != 1: + parser.error("Exactly one argument (a build target) is required") + else: + if len(args) != 2: + parser.error("Exactly two arguments (a build target and a SCM URL) are required") + activate_session(session, options) + target = args[0] + build_target = session.getBuildTarget(target) + if not build_target: + parser.error("No such build target: %s" % target) + dest_tag = session.getTag(build_target['dest_tag']) + if not dest_tag: + parser.error("No such destination tag: %s" % build_target['dest_tag_name']) + if dest_tag['locked'] and not build_opts.scratch: + parser.error("Destination tag %s is locked" % dest_tag['name']) + if build_opts.inis: + try: + params = koji.util.parse_maven_param(build_opts.inis, scratch=build_opts.scratch, + section=build_opts.section) + except ValueError as e: + parser.error(e.args[0]) + opts = to_list(params.values())[0] + if opts.pop('type', 'maven') != 'maven': + parser.error("Section %s does not contain a maven-build config" % + to_list(params.keys())[0]) + source = opts.pop('scmurl') + else: + source = args[1] + opts = koji.util.maven_opts(build_opts, scratch=build_opts.scratch) + if '://' not in source: + parser.error("No such SCM URL: %s" % source) + if build_opts.debug: + opts.setdefault('maven_options', []).append('--debug') + if build_opts.skip_tag: + opts['skip_tag'] = True + priority = None + if build_opts.background: + # relative to koji.PRIO_DEFAULT + priority = 5 + task_id = session.mavenBuild(source, target, opts, priority=priority) + if not build_opts.quiet: + print("Created task: %d" % task_id) + print("Task info: %s/taskinfo?taskID=%s" % (options.weburl, task_id)) + if build_opts.wait or (build_opts.wait is None and not _running_in_bg()): + session.logout() + return watch_tasks(session, [task_id], quiet=build_opts.quiet, + poll_interval=options.poll_interval, topurl=options.topurl) + + diff --git a/cli/koji_cli/commands/cmd_maven_chain.py b/cli/koji_cli/commands/cmd_maven_chain.py new file mode 100644 index 0000000..d1f9666 --- /dev/null +++ b/cli/koji_cli/commands/cmd_maven_chain.py @@ -0,0 +1,62 @@ +from __future__ import absolute_import, division + +from optparse import OptionParser + + +import koji + +from koji_cli.lib import ( + _running_in_bg, + activate_session, + get_usage_str, + watch_tasks +) + + +def handle_maven_chain(options, session, args): + "[build] Run a set of Maven builds in dependency order" + usage = "usage: %prog maven-chain [options] [ ...]" + parser = OptionParser(usage=get_usage_str(usage)) + parser.add_option("--skip-tag", action="store_true", help="Do not attempt to tag builds") + parser.add_option("--scratch", action="store_true", help="Perform scratch builds") + parser.add_option("--debug", action="store_true", help="Run Maven build in debug mode") + parser.add_option("--force", action="store_true", help="Force rebuilds of all packages") + parser.add_option("--wait", action="store_true", + help="Wait on build, even if running in the background") + parser.add_option("--nowait", action="store_false", dest="wait", help="Don't wait on build") + parser.add_option("--background", action="store_true", + help="Run the build at a lower priority") + (build_opts, args) = parser.parse_args(args) + if len(args) < 2: + parser.error("Two arguments (a build target and a config file) are required") + activate_session(session, options) + target = args[0] + build_target = session.getBuildTarget(target) + if not build_target: + parser.error("No such build target: %s" % target) + dest_tag = session.getTag(build_target['dest_tag']) + if not dest_tag: + parser.error("No such destination tag: %s" % build_target['dest_tag_name']) + if dest_tag['locked'] and not build_opts.scratch: + parser.error("Destination tag %s is locked" % dest_tag['name']) + opts = {} + for key in ('skip_tag', 'scratch', 'debug', 'force'): + val = getattr(build_opts, key) + if val: + opts[key] = val + try: + builds = koji.util.parse_maven_chain(args[1:], scratch=opts.get('scratch')) + except ValueError as e: + parser.error(e.args[0]) + priority = None + if build_opts.background: + priority = 5 + task_id = session.chainMaven(builds, target, opts, priority=priority) + print("Created task: %d" % task_id) + print("Task info: %s/taskinfo?taskID=%s" % (options.weburl, task_id)) + if build_opts.wait or (build_opts.wait is None and not _running_in_bg()): + session.logout() + return watch_tasks(session, [task_id], quiet=options.quiet, + poll_interval=options.poll_interval, topurl=options.topurl) + + diff --git a/cli/koji_cli/commands/cmd_mock_config.py b/cli/koji_cli/commands/cmd_mock_config.py new file mode 100644 index 0000000..8b276bf --- /dev/null +++ b/cli/koji_cli/commands/cmd_mock_config.py @@ -0,0 +1,168 @@ +from __future__ import absolute_import, division + +from optparse import OptionParser + + +import koji + +from koji_cli.lib import ( + ensure_connection, + error, + get_usage_str, + warn +) + + +def anon_handle_mock_config(goptions, session, args): + "[info] Create a mock config" + usage = "usage: %prog mock-config [options]" + parser = OptionParser(usage=get_usage_str(usage)) + parser.add_option("-a", "--arch", help="Specify the arch") + parser.add_option("-n", "--name", help="Specify the name for the buildroot") + parser.add_option("--tag", help="Create a mock config for a tag") + parser.add_option("--target", help="Create a mock config for a build target") + parser.add_option("--task", help="Duplicate the mock config of a previous task") + parser.add_option("--latest", action="store_true", help="use the latest redirect url") + parser.add_option("--buildroot", + help="Duplicate the mock config for the specified buildroot id") + parser.add_option("--mockdir", default="/var/lib/mock", metavar="DIR", help="Specify mockdir") + parser.add_option("--topdir", metavar="DIR", + help="Specify topdir, topdir tops the topurl") + parser.add_option("--topurl", metavar="URL", + help="URL under which Koji files are accessible, " + "when topdir is specified, topdir tops the topurl") + parser.add_option("--distribution", default="Koji Testing", + help="Change the distribution macro") + parser.add_option("--yum-proxy", help="Specify a yum proxy") + parser.add_option("-o", metavar="FILE", dest="ofile", help="Output to a file") + (options, args) = parser.parse_args(args) + ensure_connection(session, goptions) + if args: + # for historical reasons, we also accept buildroot name as first arg + if not options.name: + options.name = args[0] + else: + parser.error("Name already specified via option") + arch = None + opts = {} + for k in ('topdir', 'topurl', 'distribution', 'mockdir', 'yum_proxy'): + if hasattr(options, k): + if getattr(options, k) is not None: + opts[k] = getattr(options, k) + if opts.get('topdir') and opts.get('topurl'): + del opts['topurl'] + if not opts.get('topdir') and not opts.get('topurl'): + opts['topurl'] = goptions.topurl + if options.buildroot: + try: + br_id = int(options.buildroot) + except ValueError: + parser.error("Buildroot id must be an integer") + brootinfo = session.getBuildroot(br_id) + if brootinfo is None: + error("No such buildroot: %r" % br_id) + if options.latest: + opts['repoid'] = 'latest' + else: + opts['repoid'] = brootinfo['repo_id'] + opts['tag_name'] = brootinfo['tag_name'] + arch = brootinfo['arch'] + elif options.task: + try: + task_id = int(options.task) + except ValueError: + parser.error("Task id must be an integer") + broots = session.listBuildroots(taskID=task_id) + if not broots: + error("No buildroots for task %s (or no such task)" % options.task) + if len(broots) > 1: + print("Multiple buildroots found: %s" % [br['id'] for br in broots]) + brootinfo = broots[-1] + if options.latest: + opts['repoid'] = 'latest' + else: + opts['repoid'] = brootinfo['repo_id'] + opts['tag_name'] = brootinfo['tag_name'] + arch = brootinfo['arch'] + if not options.name: + options.name = "%s-task_%i" % (opts['tag_name'], task_id) + elif options.tag: + if not options.arch: + error("Please specify an arch") + tag = session.getTag(options.tag) + if not tag: + parser.error("No such tag: %s" % options.tag) + arch = options.arch + config = session.getBuildConfig(tag['id']) + if not config: + error("Could not get config info for tag: %(name)s" % tag) + opts['tag_name'] = tag['name'] + if options.latest: + opts['repoid'] = 'latest' + else: + repo = session.getRepo(config['id']) + if not repo: + error("Could not get a repo for tag: %(name)s" % tag) + opts['repoid'] = repo['id'] + elif options.target: + if not options.arch: + error("Please specify an arch") + arch = options.arch + target = session.getBuildTarget(options.target) + if not target: + parser.error("No such build target: %s" % options.target) + opts['tag_name'] = target['build_tag_name'] + if options.latest: + opts['repoid'] = 'latest' + else: + repo = session.getRepo(target['build_tag']) + if not repo: + error("Could not get a repo for tag: %s" % opts['tag_name']) + opts['repoid'] = repo['id'] + else: + parser.error("Please specify one of: --tag, --target, --task, --buildroot") + if options.name: + name = options.name + else: + name = "%(tag_name)s-repo_%(repoid)s" % opts + + event = None + if opts['repoid'] != 'latest': + event = session.repoInfo(opts['repoid'])['create_event'] + buildcfg = session.getBuildConfig(opts['tag_name'], event=event) + if arch: + if not buildcfg['arches']: + warn("Tag %s has an empty arch list" % opts['tag_name']) + elif arch not in buildcfg['arches']: + warn('%s is not in the list of tag arches' % arch) + if 'mock.forcearch' in buildcfg['extra']: + if bool(buildcfg['extra']['mock.forcearch']): + opts['forcearch'] = arch + if 'mock.package_manager' in buildcfg['extra']: + opts['package_manager'] = buildcfg['extra']['mock.package_manager'] + if 'mock.yum.module_hotfixes' in buildcfg['extra']: + opts['module_hotfixes'] = buildcfg['extra']['mock.yum.module_hotfixes'] + if 'mock.yum.best' in buildcfg['extra']: + opts['yum_best'] = int(buildcfg['extra']['mock.yum.best']) + if 'mock.bootstrap_image' in buildcfg['extra']: + opts['use_bootstrap_image'] = True + opts['bootstrap_image'] = buildcfg['extra']['mock.bootstrap_image'] + if 'mock.use_bootstrap' in buildcfg['extra']: + opts['use_bootstrap'] = buildcfg['extra']['mock.use_bootstrap'] + if 'mock.module_setup_commands' in buildcfg['extra']: + opts['module_setup_commands'] = buildcfg['extra']['mock.module_setup_commands'] + if 'mock.releasever' in buildcfg['extra']: + opts['releasever'] = buildcfg['extra']['mock.releasever'] + opts['tag_macros'] = {} + for key in buildcfg['extra']: + if key.startswith('rpm.macro.'): + macro = '%' + key[10:] + opts['tag_macros'][macro] = buildcfg['extra'][key] + output = koji.genMockConfig(name, arch, **opts) + if options.ofile: + with open(options.ofile, 'wt') as fo: + fo.write(output) + else: + print(output) + + diff --git a/cli/koji_cli/commands/cmd_moshimoshi.py b/cli/koji_cli/commands/cmd_moshimoshi.py new file mode 100644 index 0000000..eabb8af --- /dev/null +++ b/cli/koji_cli/commands/cmd_moshimoshi.py @@ -0,0 +1,41 @@ +from __future__ import absolute_import, division + +import random +from optparse import OptionParser + + +import koji + +from koji_cli.lib import ( + activate_session, + get_usage_str, + greetings +) + + +def handle_moshimoshi(options, session, args): + "[misc] Introduce yourself" + usage = "usage: %prog moshimoshi [options]" + parser = OptionParser(usage=get_usage_str(usage)) + (opts, args) = parser.parse_args(args) + if len(args) != 0: + parser.error("This command takes no arguments") + activate_session(session, options) + u = session.getLoggedInUser() + if not u: + print("Not authenticated") + u = {'name': 'anonymous user'} + print("%s, %s!" % (_printable_unicode(random.choice(greetings)), u["name"])) + print("") + print("You are using the hub at %s" % session.baseurl) + authtype = u.get('authtype', getattr(session, 'authtype', None)) + if authtype == koji.AUTHTYPE_NORMAL: + print("Authenticated via password") + elif authtype == koji.AUTHTYPE_GSSAPI: + print("Authenticated via GSSAPI") + elif authtype == koji.AUTHTYPE_KERB: + print("Authenticated via Kerberos principal %s" % session.krb_principal) + elif authtype == koji.AUTHTYPE_SSL: + print("Authenticated via client certificate %s" % options.cert) + + diff --git a/cli/koji_cli/commands/cmd_move_build.py b/cli/koji_cli/commands/cmd_move_build.py new file mode 100644 index 0000000..20c743a --- /dev/null +++ b/cli/koji_cli/commands/cmd_move_build.py @@ -0,0 +1,67 @@ +from __future__ import absolute_import, division + +from optparse import OptionParser + + +import koji + +from koji_cli.lib import ( + _running_in_bg, + activate_session, + get_usage_str, + watch_tasks +) + + +def handle_move_build(opts, session, args): + "[bind] 'Move' one or more builds between tags" + usage = "usage: %prog move-build [options] [ ...]" + parser = OptionParser(usage=get_usage_str(usage)) + parser.add_option("--force", action="store_true", help="force operation") + parser.add_option("--wait", action="store_true", + help="Wait on tasks, even if running in the background") + parser.add_option("--nowait", action="store_false", dest="wait", + help="Do not wait on tasks") + parser.add_option("--all", action="store_true", + help="move all instances of a package, 's are package names") + (options, args) = parser.parse_args(args) + if len(args) < 3: + if options.all: + parser.error( + "This command, with --all, takes at least three arguments: two tags and one or " + "more package names") + else: + parser.error( + "This command takes at least three arguments: two tags and one or more package " + "n-v-r's") + activate_session(session, opts) + tasks = [] + builds = [] + + if options.all: + for arg in args[2:]: + pkg = session.getPackage(arg) + if not pkg: + print("No such package: %s, skipping." % arg) + continue + tasklist = session.moveAllBuilds(args[0], args[1], arg, options.force) + tasks.extend(tasklist) + else: + for arg in args[2:]: + build = session.getBuild(arg) + if not build: + print("No such build: %s, skipping." % arg) + continue + if build not in builds: + builds.append(build) + + for build in builds: + task_id = session.moveBuild(args[0], args[1], build['id'], options.force) + tasks.append(task_id) + print("Created task %d, moving %s" % (task_id, koji.buildLabel(build))) + if options.wait or (options.wait is None and not _running_in_bg()): + session.logout() + return watch_tasks(session, tasks, quiet=opts.quiet, + poll_interval=opts.poll_interval, topurl=opts.topurl) + + diff --git a/cli/koji_cli/commands/cmd_prune_signed_copies.py b/cli/koji_cli/commands/cmd_prune_signed_copies.py new file mode 100644 index 0000000..fb4ac0c --- /dev/null +++ b/cli/koji_cli/commands/cmd_prune_signed_copies.py @@ -0,0 +1,373 @@ +from __future__ import absolute_import, division + +import fnmatch +import os +import stat +import time +from optparse import SUPPRESS_HELP, OptionParser + +import six +import six.moves.xmlrpc_client +from six.moves import range, zip + +import koji +from koji.util import to_list + +from koji_cli.lib import ( + activate_session, + get_usage_str, + warn +) + + +def handle_prune_signed_copies(goptions, session, args): + "[admin] Prune signed copies" + usage = "usage: %prog prune-signed-copies [options]" + parser = OptionParser(usage=get_usage_str(usage)) + parser.add_option("-n", "--test", action="store_true", help="Test mode") + parser.add_option("-v", "--verbose", action="store_true", help="Be more verbose") + parser.add_option("--days", type="int", default=5, help="Timeout before clearing") + parser.add_option("-p", "--package", "--pkg", help="Limit to a single package") + parser.add_option("-b", "--build", help="Limit to a single build") + parser.add_option("-i", "--ignore-tag", action="append", default=[], + help="Ignore these tags when considering whether a build is/was latest") + parser.add_option("--ignore-tag-file", + help="File to read tag ignore patterns from") + parser.add_option("-r", "--protect-tag", action="append", default=[], + help="Do not prune signed copies from matching tags") + parser.add_option("--protect-tag-file", + help="File to read tag protect patterns from") + parser.add_option("--trashcan-tag", default="trashcan", help="Specify trashcan tag") + # Don't use local debug option, this one stays here for backward compatibility + # https://pagure.io/koji/issue/2084 + parser.add_option("--debug", action="store_true", default=goptions.debug, help=SUPPRESS_HELP) + (options, args) = parser.parse_args(args) + # different ideas/modes + # 1) remove all signed copies of builds that are not latest for some tag + # 2) remove signed copies when a 'better' signature is available + # 3) for a specified tag, remove all signed copies that are not latest (w/ inheritance) + # 4) for a specified tag, remove all signed copies (no inheritance) + # (but skip builds that are multiply tagged) + + # for now, we're just implementing mode #1 + # (with the modification that we check to see if the build was latest within + # the last N days) + if options.ignore_tag_file: + with open(options.ignore_tag_file) as fo: + options.ignore_tag.extend([line.strip() for line in fo.readlines()]) + if options.protect_tag_file: + with open(options.protect_tag_file) as fo: + options.protect_tag.extend([line.strip() for line in fo.readlines()]) + if options.debug: + options.verbose = True + cutoff_ts = time.time() - options.days * 24 * 3600 + if options.debug: + print("Cutoff date: %s" % time.asctime(time.localtime(cutoff_ts))) + activate_session(session, goptions) + if not options.build: + if options.verbose: + print("Getting builds...") + qopts = { + 'state': koji.BUILD_STATES['COMPLETE'], + 'queryOpts': { + 'limit': 50000, + 'offset': 0, + 'order': 'build_id', + } + } + if options.package: + pkginfo = session.getPackage(options.package) + qopts['packageID'] = pkginfo['id'] + builds = [] + while True: + chunk = [(b['nvr'], b) for b in session.listBuilds(**qopts)] + if not chunk: + break + builds.extend(chunk) + qopts['queryOpts']['offset'] += qopts['queryOpts']['limit'] + if options.verbose: + print("...got %i builds" % len(builds)) + builds.sort() + else: + # single build + binfo = session.getBuild(options.build) + if not binfo: + parser.error('No such build: %s' % options.build) + builds = [("%(name)s-%(version)s-%(release)s" % binfo, binfo)] + total_files = 0 + total_space = 0 + + def _histline(event_id, x): + if event_id == x['revoke_event']: + ts = x['revoke_ts'] + fmt = "Untagged %(name)s-%(version)s-%(release)s from %(tag_name)s" + elif event_id == x['create_event']: + ts = x['create_ts'] + fmt = "Tagged %(name)s-%(version)s-%(release)s with %(tag_name)s" + if x['active']: + fmt += " [still active]" + else: + raise koji.GenericError("No such event: (%r, %r)" % (event_id, x)) + time_str = time.asctime(time.localtime(ts)) + return "%s: %s" % (time_str, fmt % x) + for nvr, binfo in builds: + # listBuilds returns slightly different data than normal + if 'id' not in binfo: + binfo['id'] = binfo['build_id'] + if 'name' not in binfo: + binfo['name'] = binfo['package_name'] + if options.debug: + print("DEBUG: %s" % nvr) + # see how recently this build was latest for a tag + is_latest = False + is_protected = False + last_latest = None + tags = {} + for entry in session.queryHistory(build=binfo['id'])['tag_listing']: + # we used queryHistory rather than listTags so we can consider tags + # that the build was recently untagged from + tags.setdefault(entry['tag.name'], 1) + if options.debug: + print("Tags: %s" % to_list(tags.keys())) + for tag_name in tags: + if tag_name == options.trashcan_tag: + if options.debug: + print("Ignoring trashcan tag for build %s" % nvr) + continue + ignore_tag = False + for pattern in options.ignore_tag: + if fnmatch.fnmatch(tag_name, pattern): + if options.debug: + print("Ignoring tag %s for build %s" % (tag_name, nvr)) + ignore_tag = True + break + if ignore_tag: + continue + # in order to determine how recently this build was latest, we have + # to look at the tagging history. + hist = session.queryHistory(tag=tag_name, package=binfo['name'])['tag_listing'] + if not hist: + # really shouldn't happen + raise koji.GenericError("No history found for %s in %s" % (nvr, tag_name)) + timeline = [] + for x in hist: + # note that for revoked entries, we're effectively splitting them into + # two parts: creation and revocation. + timeline.append((x['create_event'], 1, x)) + # at the same event, revokes happen first + if x['revoke_event'] is not None: + timeline.append((x['revoke_event'], 0, x)) + timeline.sort(key=lambda entry: entry[:2]) + # find most recent creation entry for our build and crop there + latest_ts = None + for i in range(len(timeline) - 1, -1, -1): + # searching in reverse cronological order + event_id, is_create, entry = timeline[i] + if entry['build_id'] == binfo['id'] and is_create: + latest_ts = event_id + break + if not latest_ts: + # really shouldn't happen + raise koji.GenericError("No creation event found for %s in %s" % (nvr, tag_name)) + our_entry = entry + if options.debug: + print(_histline(event_id, our_entry)) + # now go through the events since most recent creation entry + timeline = timeline[i + 1:] + if not timeline: + is_latest = True + if options.debug: + print("%s is latest in tag %s" % (nvr, tag_name)) + break + # before we go any further, is this a protected tag? + protect_tag = False + for pattern in options.protect_tag: + if fnmatch.fnmatch(tag_name, pattern): + protect_tag = True + break + if protect_tag: + # we use the same time limit as for the latest calculation + # if this build was in this tag within that limit, then we will + # not prune its signed copies + if our_entry['revoke_event'] is None: + # we're still tagged with a protected tag + if options.debug: + print("Build %s has protected tag %s" % (nvr, tag_name)) + is_protected = True + break + elif our_entry['revoke_ts'] > cutoff_ts: + # we were still tagged here sometime before the cutoff + if options.debug: + print("Build %s had protected tag %s until %s" + % (nvr, tag_name, + time.asctime(time.localtime(our_entry['revoke_ts'])))) + is_protected = True + break + replaced_ts = None + revoke_ts = None + others = {} + for event_id, is_create, entry in timeline: + # So two things can knock this build from the title of latest: + # - it could be untagged (entry revoked) + # - another build could become latest (replaced) + # Note however that if the superceding entry is itself revoked, then + # our build could become latest again + if options.debug: + print(_histline(event_id, entry)) + if entry['build_id'] == binfo['id']: + if is_create: + # shouldn't happen + raise koji.GenericError("Duplicate creation event found for %s in %s" + % (nvr, tag_name)) + else: + # we've been revoked + revoke_ts = entry['revoke_ts'] + break + else: + if is_create: + # this build has become latest + replaced_ts = entry['create_ts'] + if entry['active']: + # this entry not revoked yet, so we're done for this tag + break + # since this entry is revoked later, our build might eventually be + # uncovered, so we have to keep looking + others[entry['build_id']] = 1 + else: + # other build revoked + # see if our build has resurfaced + if entry['build_id'] in others: + del others[entry['build_id']] + if replaced_ts is not None and not others: + # we've become latest again + # (note: we're not revoked yet because that triggers a break above) + replaced_ts = None + latest_ts = entry['revoke_ts'] + if last_latest is None: + timestamps = [] + else: + timestamps = [last_latest] + if revoke_ts is None: + if replaced_ts is None: + # turns out we are still latest + is_latest = True + if options.debug: + print("%s is latest (again) in tag %s" % (nvr, tag_name)) + break + else: + # replaced (but not revoked) + timestamps.append(replaced_ts) + if options.debug: + print("tag %s: %s not latest (replaced %s)" + % (tag_name, nvr, time.asctime(time.localtime(replaced_ts)))) + elif replaced_ts is None: + # revoked but not replaced + timestamps.append(revoke_ts) + if options.debug: + print("tag %s: %s not latest (revoked %s)" + % (tag_name, nvr, time.asctime(time.localtime(revoke_ts)))) + else: + # revoked AND replaced + timestamps.append(min(revoke_ts, replaced_ts)) + if options.debug: + print("tag %s: %s not latest (revoked %s, replaced %s)" + % (tag_name, nvr, time.asctime(time.localtime(revoke_ts)), + time.asctime(time.localtime(replaced_ts)))) + last_latest = max(timestamps) + if last_latest > cutoff_ts: + if options.debug: + print("%s was latest past the cutoff" % nvr) + is_latest = True + break + if is_latest: + continue + if is_protected: + continue + # not latest anywhere since cutoff, so we can remove all signed copies + rpms = session.listRPMs(buildID=binfo['id']) + session.multicall = True + for rpminfo in rpms: + session.queryRPMSigs(rpm_id=rpminfo['id']) + by_sig = {} + # index by sig + for rpminfo, [sigs] in zip(rpms, session.multiCall()): + for sig in sigs: + sigkey = sig['sigkey'] + by_sig.setdefault(sigkey, []).append(rpminfo) + builddir = koji.pathinfo.build(binfo) + build_files = 0 + build_space = 0 + if not by_sig and options.debug: + print("(build has no signatures)") + for sigkey, rpms in six.iteritems(by_sig): + mycount = 0 + archdirs = {} + sigdirs = {} + for rpminfo in rpms: + signedpath = "%s/%s" % (builddir, koji.pathinfo.signed(rpminfo, sigkey)) + try: + st = os.lstat(signedpath) + except OSError: + continue + if not stat.S_ISREG(st.st_mode): + # warn about this + print("Skipping %s. Not a regular file" % signedpath) + continue + if st.st_mtime > cutoff_ts: + print("Skipping %s. File newer than cutoff" % signedpath) + continue + if options.test: + print("Would have unlinked: %s" % signedpath) + else: + if options.verbose: + print("Unlinking: %s" % signedpath) + try: + os.unlink(signedpath) + except OSError as e: + print("Error removing %s: %s" % (signedpath, e)) + print("This script needs write access to %s" % koji.BASEDIR) + continue + mycount += 1 + build_files += 1 + build_space += st.st_size + # XXX - this makes some layout assumptions, but + # pathinfo doesn't report what we need + mydir = os.path.dirname(signedpath) + archdirs[mydir] = 1 + sigdirs[os.path.dirname(mydir)] = 1 + for dir in archdirs: + if options.test: + print("Would have removed dir: %s" % dir) + else: + if options.verbose: + print("Removing dir: %s" % dir) + try: + os.rmdir(dir) + except OSError as e: + print("Error removing %s: %s" % (signedpath, e)) + if len(sigdirs) == 1: + dir = to_list(sigdirs.keys())[0] + if options.test: + print("Would have removed dir: %s" % dir) + else: + if options.verbose: + print("Removing dir: %s" % dir) + try: + os.rmdir(dir) + except OSError as e: + print("Error removing %s: %s" % (signedpath, e)) + elif len(sigdirs) > 1: + warn("More than one signature dir for %s: %r" % (sigkey, sigdirs)) + if build_files: + total_files += build_files + total_space += build_space + if options.verbose: + print("Build: %s, Removed %i signed copies (%i bytes). Total: %i/%i" + % (nvr, build_files, build_space, total_files, total_space)) + elif options.debug and by_sig: + print("(build has no signed copies)") + print("--- Grand Totals ---") + print("Files: %i" % total_files) + print("Bytes: %i" % total_space) + + diff --git a/cli/koji_cli/commands/cmd_regen_repo.py b/cli/koji_cli/commands/cmd_regen_repo.py new file mode 100644 index 0000000..954f043 --- /dev/null +++ b/cli/koji_cli/commands/cmd_regen_repo.py @@ -0,0 +1,73 @@ +from __future__ import absolute_import, division + +from optparse import OptionParser + + + +from koji_cli.lib import ( + _running_in_bg, + activate_session, + get_usage_str, + warn, + watch_tasks +) + + +def handle_regen_repo(options, session, args): + "[admin] Force a repo to be regenerated" + usage = "usage: %prog regen-repo [options] " + parser = OptionParser(usage=get_usage_str(usage)) + parser.add_option("--target", action="store_true", + help="Interpret the argument as a build target name") + parser.add_option("--wait", action="store_true", + help="Wait on for regen to finish, even if running in the background") + parser.add_option("--nowait", action="store_false", dest="wait", + help="Don't wait on for regen to finish") + parser.add_option("--debuginfo", action="store_true", help="Include debuginfo rpms in repo") + parser.add_option("--source", "--src", action="store_true", + help="Include source rpms in each of repos") + parser.add_option("--separate-source", "--separate-src", action="store_true", + help="Include source rpms in separate src repo") + (suboptions, args) = parser.parse_args(args) + if len(args) == 0: + parser.error("A tag name must be specified") + elif len(args) > 1: + if suboptions.target: + parser.error("Only a single target may be specified") + else: + parser.error("Only a single tag name may be specified") + activate_session(session, options) + tag = args[0] + repo_opts = {} + if suboptions.target: + info = session.getBuildTarget(tag) + if not info: + parser.error("No such build target: %s" % tag) + tag = info['build_tag_name'] + info = session.getTag(tag, strict=True) + else: + info = session.getTag(tag) + if not info: + parser.error("No such tag: %s" % tag) + tag = info['name'] + targets = session.getBuildTargets(buildTagID=info['id']) + if not targets: + warn("%s is not a build tag" % tag) + if not info['arches']: + warn("Tag %s has an empty arch list" % info['name']) + if suboptions.debuginfo: + repo_opts['debuginfo'] = True + if suboptions.source: + repo_opts['src'] = True + if suboptions.separate_source: + repo_opts['separate_src'] = True + task_id = session.newRepo(tag, **repo_opts) + print("Regenerating repo for tag: %s" % tag) + print("Created task: %d" % task_id) + print("Task info: %s/taskinfo?taskID=%s" % (options.weburl, task_id)) + if suboptions.wait or (suboptions.wait is None and not _running_in_bg()): + session.logout() + return watch_tasks(session, [task_id], quiet=options.quiet, + poll_interval=options.poll_interval, topurl=options.topurl) + + diff --git a/cli/koji_cli/commands/cmd_remove_channel.py b/cli/koji_cli/commands/cmd_remove_channel.py new file mode 100644 index 0000000..50ad595 --- /dev/null +++ b/cli/koji_cli/commands/cmd_remove_channel.py @@ -0,0 +1,29 @@ +from __future__ import absolute_import, division + +from optparse import OptionParser + + + +from koji_cli.lib import ( + activate_session, + error, + get_usage_str +) + + +def handle_remove_channel(goptions, session, args): + "[admin] Remove a channel entirely" + usage = "usage: %prog remove-channel [options] " + parser = OptionParser(usage=get_usage_str(usage)) + parser.add_option("--force", action="store_true", help="force removal, if possible") + (options, args) = parser.parse_args(args) + print("remove-channel is deprecated and will be removed in 1.30") + if len(args) != 1: + parser.error("Incorrect number of arguments") + activate_session(session, goptions) + cinfo = session.getChannel(args[0]) + if not cinfo: + error("No such channel: %s" % args[0]) + session.removeChannel(args[0], force=options.force) + + diff --git a/cli/koji_cli/commands/cmd_remove_external_repo.py b/cli/koji_cli/commands/cmd_remove_external_repo.py new file mode 100644 index 0000000..b1f41c2 --- /dev/null +++ b/cli/koji_cli/commands/cmd_remove_external_repo.py @@ -0,0 +1,56 @@ +from __future__ import absolute_import, division + +from optparse import OptionParser + + + +from koji_cli.lib import ( + activate_session, + error, + get_usage_str, + warn +) + + +def handle_remove_external_repo(goptions, session, args): + "[admin] Remove an external repo from a tag or tags, or remove entirely" + usage = "usage: %prog remove-external-repo [ ...]" + parser = OptionParser(usage=get_usage_str(usage)) + parser.add_option("--alltags", action="store_true", help="Remove from all tags") + parser.add_option("--force", action='store_true', help="Force action") + (options, args) = parser.parse_args(args) + if len(args) < 1: + parser.error("Incorrect number of arguments") + activate_session(session, goptions) + repo = args[0] + tags = args[1:] + delete = not bool(tags) + data = session.getTagExternalRepos(repo_info=repo) + current_tags = [d['tag_name'] for d in data] + if options.alltags: + delete = False + if tags: + parser.error("Do not specify tags when using --alltags") + if not current_tags: + if options.force: + delete = True + else: + warn("External repo %s not associated with any tags" % repo) + return 0 + tags = current_tags + if delete: + # removing entirely + if current_tags and not options.force: + warn("Error: external repo %s used by tag(s): %s" % (repo, ', '.join(current_tags))) + error("Use --force to remove anyway") + session.deleteExternalRepo(args[0]) + else: + for tag in tags: + if tag not in current_tags: + print("External repo %s not associated with tag %s" % (repo, tag)) + continue + session.removeExternalRepoFromTag(tag, repo) + + +# This handler is for spinning livecd images +# diff --git a/cli/koji_cli/commands/cmd_remove_group.py b/cli/koji_cli/commands/cmd_remove_group.py new file mode 100644 index 0000000..77b1fc2 --- /dev/null +++ b/cli/koji_cli/commands/cmd_remove_group.py @@ -0,0 +1,39 @@ +from __future__ import absolute_import, division + +from optparse import OptionParser + + + +from koji_cli.lib import ( + activate_session, + error, + get_usage_str +) + + +def handle_remove_group(goptions, session, args): + "[admin] Remove group from tag" + usage = "usage: %prog remove-group " + parser = OptionParser(usage=get_usage_str(usage)) + (options, args) = parser.parse_args(args) + if len(args) != 2: + parser.error("Please specify a tag name and a group name") + tag = args[0] + group = args[1] + + activate_session(session, goptions) + if not (session.hasPerm('admin') or session.hasPerm('tag')): + parser.error("This action requires tag or admin privileges") + + dsttag = session.getTag(tag) + if not dsttag: + error("No such tag: %s" % tag) + + groups = dict([(p['name'], p['group_id']) for p in session.getTagGroups(tag, inherit=False)]) + group_id = groups.get(group, None) + if group_id is None: + error("Group %s doesn't exist within tag %s" % (group, tag)) + + session.groupListRemove(tag, group) + + diff --git a/cli/koji_cli/commands/cmd_remove_host_from_channel.py b/cli/koji_cli/commands/cmd_remove_host_from_channel.py new file mode 100644 index 0000000..6241fb8 --- /dev/null +++ b/cli/koji_cli/commands/cmd_remove_host_from_channel.py @@ -0,0 +1,34 @@ +from __future__ import absolute_import, division + +from optparse import OptionParser + + + +from koji_cli.lib import ( + activate_session, + error, + get_usage_str +) + + +def handle_remove_host_from_channel(goptions, session, args): + "[admin] Remove a host from a channel" + usage = "usage: %prog remove-host-from-channel [options] " + parser = OptionParser(usage=get_usage_str(usage)) + (options, args) = parser.parse_args(args) + if len(args) != 2: + parser.error("Please specify a hostname and a channel") + host = args[0] + activate_session(session, goptions) + hostinfo = session.getHost(host) + if not hostinfo: + error("No such host: %s" % host) + hostchannels = [c['name'] for c in session.listChannels(hostinfo['id'])] + + channel = args[1] + if channel not in hostchannels: + error("Host %s is not a member of channel %s" % (host, channel)) + + session.removeHostFromChannel(host, channel) + + diff --git a/cli/koji_cli/commands/cmd_remove_notification.py b/cli/koji_cli/commands/cmd_remove_notification.py new file mode 100644 index 0000000..7e07b6c --- /dev/null +++ b/cli/koji_cli/commands/cmd_remove_notification.py @@ -0,0 +1,34 @@ +from __future__ import absolute_import, division + +from optparse import OptionParser + + + +from koji_cli.lib import ( + activate_session, + get_usage_str +) + + +def handle_remove_notification(goptions, session, args): + "[monitor] Remove user's notifications" + usage = "usage: %prog remove-notification [options] [ ...]" + parser = OptionParser(usage=get_usage_str(usage)) + (options, args) = parser.parse_args(args) + + activate_session(session, goptions) + + if len(args) < 1: + parser.error("At least one notification id has to be specified") + + try: + n_ids = [int(x) for x in args] + except ValueError: + parser.error("All notification ids has to be integers") + + for n_id in n_ids: + session.deleteNotification(n_id) + if not goptions.quiet: + print("Notification %d successfully removed." % n_id) + + diff --git a/cli/koji_cli/commands/cmd_remove_pkg.py b/cli/koji_cli/commands/cmd_remove_pkg.py new file mode 100644 index 0000000..1956214 --- /dev/null +++ b/cli/koji_cli/commands/cmd_remove_pkg.py @@ -0,0 +1,51 @@ +from __future__ import absolute_import, division + +from optparse import OptionParser + + +import koji + +from koji_cli.lib import ( + activate_session, + error, + get_usage_str, + warn +) + + +def handle_remove_pkg(goptions, session, args): + "[admin] Remove a package from the listing for tag" + usage = "usage: %prog remove-pkg [options] [ ...]" + parser = OptionParser(usage=get_usage_str(usage)) + parser.add_option("--force", action='store_true', help="Override blocks if necessary") + (options, args) = parser.parse_args(args) + if len(args) < 2: + parser.error("Please specify a tag and at least one package") + activate_session(session, goptions) + tag = args[0] + opts = {} + opts['force'] = options.force + # check if list of packages exists for that tag already + dsttag = session.getTag(tag) + if dsttag is None: + error("No such tag: %s" % tag) + try: + pkglist = session.listPackages(tagID=dsttag['id'], with_owners=False) + except koji.ParameterError: + # performance option added in 1.25 + pkglist = session.listPackages(tagID=dsttag['id']) + pkglist = dict([(p['package_name'], p['package_id']) for p in pkglist]) + ret = 0 + for package in args[1:]: + package_id = pkglist.get(package, None) + if package_id is None: + warn("Package %s is not in tag %s" % (package, tag)) + ret = 1 + if ret: + error(code=ret) + session.multicall = True + for package in args[1:]: + session.packageListRemove(tag, package, **opts) + session.multiCall(strict=True) + + diff --git a/cli/koji_cli/commands/cmd_remove_sig.py b/cli/koji_cli/commands/cmd_remove_sig.py new file mode 100644 index 0000000..4d19dfa --- /dev/null +++ b/cli/koji_cli/commands/cmd_remove_sig.py @@ -0,0 +1,47 @@ +from __future__ import absolute_import, division + +from optparse import OptionParser + + +import koji + +from koji_cli.lib import ( + activate_session, + error, + get_usage_str +) + + +def handle_remove_sig(goptions, session, args): + "[admin] Remove signed RPMs from db and disk" + usage = "usage: %prog remove-sig [options] " + description = "Only use this method in extreme situations, because it " + description += "goes against Koji's design of immutable, auditable data." + parser = OptionParser(usage=get_usage_str(usage), description=description) + parser.add_option("--sigkey", action="store", default=None, help="Specify signature key") + parser.add_option("--all", action="store_true", default=False, + help="Remove all signed copies for specified RPM") + (options, args) = parser.parse_args(args) + if len(args) < 1: + parser.error("Please specify an RPM") + + if not options.all and not options.sigkey: + error("Either --sigkey or --all options must be given") + + if options.all and options.sigkey: + error("Conflicting options specified") + + activate_session(session, goptions) + rpminfo = args[0] + + try: + session.deleteRPMSig(rpminfo, sigkey=options.sigkey, all_sigs=options.all) + except koji.GenericError as e: + msg = str(e) + if msg.startswith("No such rpm"): + # make this a little more readable than the hub error + error("No such rpm in system: %s" % rpminfo) + else: + error("Signature removal failed: %s" % msg) + + diff --git a/cli/koji_cli/commands/cmd_remove_tag.py b/cli/koji_cli/commands/cmd_remove_tag.py new file mode 100644 index 0000000..12ce40a --- /dev/null +++ b/cli/koji_cli/commands/cmd_remove_tag.py @@ -0,0 +1,34 @@ +from __future__ import absolute_import, division + +from optparse import OptionParser + + + +from koji_cli.lib import ( + activate_session, + error, + get_usage_str +) + + +def handle_remove_tag(goptions, session, args): + "[admin] Remove a tag" + usage = "usage: %prog remove-tag [options] " + parser = OptionParser(usage=get_usage_str(usage)) + (options, args) = parser.parse_args(args) + + if len(args) != 1: + parser.error("Please specify a tag to remove") + activate_session(session, goptions) + + if not (session.hasPerm('admin') or session.hasPerm('tag')): + parser.error("This action requires tag or admin privileges") + + tag = args[0] + tag_info = session.getTag(tag) + if not tag_info: + error("No such tag: %s" % tag) + + session.deleteTag(tag_info['id']) + + diff --git a/cli/koji_cli/commands/cmd_remove_tag_inheritance.py b/cli/koji_cli/commands/cmd_remove_tag_inheritance.py new file mode 100644 index 0000000..718da62 --- /dev/null +++ b/cli/koji_cli/commands/cmd_remove_tag_inheritance.py @@ -0,0 +1,71 @@ +from __future__ import absolute_import, division + +from optparse import OptionParser + + + +from koji_cli.lib import ( + activate_session, + error, + get_usage_str +) + + +def handle_remove_tag_inheritance(goptions, session, args): + """[admin] Remove a tag inheritance link""" + usage = "usage: %prog remove-tag-inheritance " + parser = OptionParser(usage=get_usage_str(usage)) + (options, args) = parser.parse_args(args) + + if len(args) < 1: + parser.error("This command takes at least one argument: a tag name or ID") + + if len(args) > 3: + parser.error("This command takes at most three argument: a tag name or ID, a parent tag " + "name or ID, and a priority") + + activate_session(session, goptions) + + tag = session.getTag(args[0]) + if not tag: + parser.error("No such tag: %s" % args[0]) + + parent = None + priority = None + if len(args) > 1: + parent = session.getTag(args[1]) + if not parent: + parser.error("No such tag: %s" % args[1]) + if len(args) > 2: + priority = args[2] + + data = session.getInheritanceData(tag['id']) + if parent and data: + data = [datum for datum in data if datum['parent_id'] == parent['id']] + if priority and data: + data = [datum for datum in data if datum['priority'] == priority] + + if len(data) == 0: + error("No inheritance link found to remove. Please check your arguments") + elif len(data) > 1: + print("Multiple matches for tag.") + if not parent: + error("Please specify a parent on the command line.") + if not priority: + error("Please specify a priority on the command line.") + error("Error: Key constraints may be broken. Exiting.") + + # len(data) == 1 + data = data[0] + + inheritanceData = session.getInheritanceData(tag['id']) + + new_data = data.copy() + new_data['delete link'] = True + + # find the data we want to edit and replace it + index = inheritanceData.index(data) + inheritanceData[index] = new_data + session.setInheritanceData(tag['id'], inheritanceData) + + diff --git a/cli/koji_cli/commands/cmd_remove_target.py b/cli/koji_cli/commands/cmd_remove_target.py new file mode 100644 index 0000000..ab7e437 --- /dev/null +++ b/cli/koji_cli/commands/cmd_remove_target.py @@ -0,0 +1,34 @@ +from __future__ import absolute_import, division + +from optparse import OptionParser + + + +from koji_cli.lib import ( + activate_session, + error, + get_usage_str +) + + +def handle_remove_target(goptions, session, args): + "[admin] Remove a build target" + usage = "usage: %prog remove-target [options] " + parser = OptionParser(usage=get_usage_str(usage)) + (options, args) = parser.parse_args(args) + + if len(args) != 1: + parser.error("Please specify a build target to remove") + activate_session(session, goptions) + + if not (session.hasPerm('admin') or session.hasPerm('target')): + parser.error("This action requires target or admin privileges") + + target = args[0] + target_info = session.getBuildTarget(target) + if not target_info: + error("No such build target: %s" % target) + + session.deleteBuildTarget(target_info['id']) + + diff --git a/cli/koji_cli/commands/cmd_restart_hosts.py b/cli/koji_cli/commands/cmd_restart_hosts.py new file mode 100644 index 0000000..5c65d85 --- /dev/null +++ b/cli/koji_cli/commands/cmd_restart_hosts.py @@ -0,0 +1,69 @@ +from __future__ import absolute_import, division + +from optparse import OptionParser + + +import koji + +from koji_cli.lib import ( + _running_in_bg, + activate_session, + error, + get_usage_str, + warn, + watch_tasks +) + + +def handle_restart_hosts(options, session, args): + "[admin] Restart enabled hosts" + usage = "usage: %prog restart-hosts [options]" + parser = OptionParser(usage=get_usage_str(usage)) + parser.add_option("--wait", action="store_true", + help="Wait on the task, even if running in the background") + parser.add_option("--nowait", action="store_false", dest="wait", help="Don't wait on task") + parser.add_option("--quiet", action="store_true", + help="Do not print the task information", default=options.quiet) + parser.add_option("--force", action="store_true", help="Ignore checks and force operation") + parser.add_option("--channel", help="Only hosts in this channel") + parser.add_option("--arch", "-a", action="append", default=[], + help="Limit to hosts of this architecture (can be given multiple times)") + parser.add_option("--timeout", metavar='N', type='int', help="Time out after N seconds") + (my_opts, args) = parser.parse_args(args) + + if len(args) > 0: + parser.error("restart-hosts does not accept arguments") + + activate_session(session, options) + + # check for existing restart tasks + if not my_opts.force: + query = { + 'method': 'restartHosts', + 'state': + [koji.TASK_STATES[s] for s in ('FREE', 'OPEN', 'ASSIGNED')], + } + others = session.listTasks(query) + if others: + warn('Found other restartHosts tasks running.') + warn('Task ids: %r' % [t['id'] for t in others]) + error('Use --force to run anyway') + + callopts = {} + if my_opts.channel: + callopts['channel'] = my_opts.channel + if my_opts.arch: + callopts['arches'] = my_opts.arch + if my_opts.timeout: + callopts['timeout'] = my_opts.timeout + if callopts: + task_id = session.restartHosts(options=callopts) + else: + # allow default case to work with older hub + task_id = session.restartHosts() + if my_opts.wait or (my_opts.wait is None and not _running_in_bg()): + session.logout() + return watch_tasks(session, [task_id], quiet=my_opts.quiet, + poll_interval=options.poll_interval, topurl=options.topurl) + + diff --git a/cli/koji_cli/commands/cmd_resubmit.py b/cli/koji_cli/commands/cmd_resubmit.py new file mode 100644 index 0000000..a1d52c2 --- /dev/null +++ b/cli/koji_cli/commands/cmd_resubmit.py @@ -0,0 +1,42 @@ +from __future__ import absolute_import, division + +from optparse import OptionParser + + + +from koji_cli.lib import ( + _running_in_bg, + activate_session, + get_usage_str, + watch_tasks +) + + +def handle_resubmit(goptions, session, args): + """[build] Retry a canceled or failed task, using the same parameter as the original task.""" + usage = "usage: %prog resubmit [options] " + parser = OptionParser(usage=get_usage_str(usage)) + parser.add_option("--wait", action="store_true", + help="Wait on task, even if running in the background") + parser.add_option("--nowait", action="store_false", dest="wait", help="Don't wait on task") + parser.add_option("--nowatch", action="store_true", dest="nowait", + help="An alias for --nowait") + parser.add_option("--quiet", action="store_true", default=goptions.quiet, + help="Do not print the task information") + (options, args) = parser.parse_args(args) + if len(args) != 1: + parser.error("Please specify a single task ID") + activate_session(session, goptions) + taskID = int(args[0]) + if not options.quiet: + print("Resubmitting the following task:") + _printTaskInfo(session, taskID, goptions.topdir, 0, False, True) + newID = session.resubmitTask(taskID) + if not options.quiet: + print("Resubmitted task %s as new task %s" % (taskID, newID)) + if options.wait or (options.wait is None and not _running_in_bg()): + session.logout() + return watch_tasks(session, [newID], quiet=options.quiet, + poll_interval=goptions.poll_interval, topurl=goptions.topurl) + + diff --git a/cli/koji_cli/commands/cmd_revoke_cg_access.py b/cli/koji_cli/commands/cmd_revoke_cg_access.py new file mode 100644 index 0000000..2c6ca4b --- /dev/null +++ b/cli/koji_cli/commands/cmd_revoke_cg_access.py @@ -0,0 +1,28 @@ +from __future__ import absolute_import, division + +from optparse import OptionParser + + + +from koji_cli.lib import ( + activate_session, + get_usage_str +) + + +def handle_revoke_cg_access(goptions, session, args): + "[admin] Remove a user from a content generator" + usage = "usage: %prog revoke-cg-access " + parser = OptionParser(usage=get_usage_str(usage)) + (options, args) = parser.parse_args(args) + if len(args) != 2: + parser.error("Please specify a user and content generator") + activate_session(session, goptions) + user = args[0] + cg = args[1] + uinfo = session.getUser(user) + if uinfo is None: + parser.error("No such user: %s" % user) + session.revokeCGAccess(uinfo['name'], cg) + + diff --git a/cli/koji_cli/commands/cmd_revoke_permission.py b/cli/koji_cli/commands/cmd_revoke_permission.py new file mode 100644 index 0000000..9917a30 --- /dev/null +++ b/cli/koji_cli/commands/cmd_revoke_permission.py @@ -0,0 +1,32 @@ +from __future__ import absolute_import, division + +from optparse import OptionParser + + + +from koji_cli.lib import ( + activate_session, + get_usage_str +) + + +def handle_revoke_permission(goptions, session, args): + "[admin] Revoke a permission from a user" + usage = "usage: %prog revoke-permission [ ...]" + parser = OptionParser(usage=get_usage_str(usage)) + (options, args) = parser.parse_args(args) + if len(args) < 2: + parser.error("Please specify a permission and at least one user") + activate_session(session, goptions) + perm = args[0] + names = args[1:] + users = [] + for n in names: + user = session.getUser(n) + if user is None: + parser.error("No such user: %s" % n) + users.append(user) + for user in users: + session.revokePermission(user['name'], perm) + + diff --git a/cli/koji_cli/commands/cmd_rpminfo.py b/cli/koji_cli/commands/cmd_rpminfo.py new file mode 100644 index 0000000..74263fc --- /dev/null +++ b/cli/koji_cli/commands/cmd_rpminfo.py @@ -0,0 +1,94 @@ +from __future__ import absolute_import, division + +import os +import time +from optparse import OptionParser + + +import koji + +from koji_cli.lib import ( + ensure_connection, + error, + get_usage_str, + warn +) + + +def anon_handle_rpminfo(goptions, session, args): + "[info] Print basic information about an RPM" + usage = "usage: %prog rpminfo [options] [ ...]" + parser = OptionParser(usage=get_usage_str(usage)) + parser.add_option("--buildroots", action="store_true", + help="show buildroots the rpm was used in") + (options, args) = parser.parse_args(args) + if len(args) < 1: + parser.error("Please specify an RPM") + ensure_connection(session, goptions) + error_hit = False + for rpm in args: + info = session.getRPM(rpm) + if info is None: + warn("No such rpm: %s\n" % rpm) + error_hit = True + continue + if info['epoch'] is None: + info['epoch'] = "" + else: + info['epoch'] = str(info['epoch']) + ":" + if not info.get('external_repo_id', 0): + buildinfo = session.getBuild(info['build_id']) + buildinfo['name'] = buildinfo['package_name'] + buildinfo['arch'] = 'src' + if buildinfo['epoch'] is None: + buildinfo['epoch'] = "" + else: + buildinfo['epoch'] = str(buildinfo['epoch']) + ":" + print("RPM: %(epoch)s%(name)s-%(version)s-%(release)s.%(arch)s [%(id)d]" % info) + if info.get('external_repo_id'): + repo = session.getExternalRepo(info['external_repo_id']) + print("External Repository: %(name)s [%(id)i]" % repo) + print("External Repository url: %(url)s" % repo) + else: + print("RPM Path: %s" % + os.path.join(koji.pathinfo.build(buildinfo), koji.pathinfo.rpm(info))) + print("SRPM: %(epoch)s%(name)s-%(version)s-%(release)s [%(id)d]" % buildinfo) + print("SRPM Path: %s" % + os.path.join(koji.pathinfo.build(buildinfo), koji.pathinfo.rpm(buildinfo))) + print("Built: %s" % time.strftime('%a, %d %b %Y %H:%M:%S %Z', + time.localtime(info['buildtime']))) + print("SIGMD5: %(payloadhash)s" % info) + print("Size: %(size)s" % info) + if not info.get('external_repo_id', 0): + headers = session.getRPMHeaders(rpmID=info['id'], + headers=["license"]) + if 'license' in headers: + print("License: %(license)s" % headers) + print("Build ID: %(build_id)s" % info) + if info['buildroot_id'] is None: + print("No buildroot data available") + else: + br_info = session.getBuildroot(info['buildroot_id']) + if br_info['br_type'] == koji.BR_TYPES['STANDARD']: + print("Buildroot: %(id)i (tag %(tag_name)s, arch %(arch)s, repo %(repo_id)i)" % + br_info) + print("Build Host: %(host_name)s" % br_info) + print("Build Task: %(task_id)i" % br_info) + else: + print("Content generator: %(cg_name)s" % br_info) + print("Buildroot: %(id)i" % br_info) + print("Build Host OS: %(host_os)s (%(host_arch)s)" % br_info) + if info.get('extra'): + print("Extra: %(extra)r" % info) + if options.buildroots: + br_list = session.listBuildroots(rpmID=info['id'], queryOpts={'order': 'buildroot.id'}) + print("Used in %i buildroots:" % len(br_list)) + if len(br_list): + print(" %8s %-28s %-8s %-29s" % ('id', 'build tag', 'arch', 'build host')) + print(" %s %s %s %s" % ('-' * 8, '-' * 28, '-' * 8, '-' * 29)) + for br_info in br_list: + print(" %(id)8i %(tag_name)-28s %(arch)-8s %(host_name)-29s" % br_info) + if error_hit: + error() + + diff --git a/cli/koji_cli/commands/cmd_search.py b/cli/koji_cli/commands/cmd_search.py new file mode 100644 index 0000000..e145060 --- /dev/null +++ b/cli/koji_cli/commands/cmd_search.py @@ -0,0 +1,39 @@ +from __future__ import absolute_import, division + +from optparse import OptionParser + + + +from koji_cli.lib import ( + ensure_connection, + get_usage_str +) + + +def anon_handle_search(goptions, session, args): + "[search] Search the system" + usage = "usage: %prog search [options] " + usage += '\nAvailable search types: %s' % ', '.join(_search_types) + parser = OptionParser(usage=get_usage_str(usage)) + parser.add_option("-r", "--regex", action="store_true", help="treat pattern as regex") + parser.add_option("--exact", action="store_true", help="exact matches only") + (options, args) = parser.parse_args(args) + if len(args) < 1: + parser.error("Please specify search type") + if len(args) < 2: + parser.error("Please specify search pattern") + type = args[0] + if type not in _search_types: + parser.error("No such search type: %s" % type) + pattern = args[1] + matchType = 'glob' + if options.regex: + matchType = 'regexp' + elif options.exact: + matchType = 'exact' + ensure_connection(session, goptions) + data = session.search(pattern, type, matchType) + for row in data: + print(row['name']) + + diff --git a/cli/koji_cli/commands/cmd_set_build_volume.py b/cli/koji_cli/commands/cmd_set_build_volume.py new file mode 100644 index 0000000..2ac5621 --- /dev/null +++ b/cli/koji_cli/commands/cmd_set_build_volume.py @@ -0,0 +1,42 @@ +from __future__ import absolute_import, division + +from optparse import OptionParser + + + +from koji_cli.lib import ( + activate_session, + error, + get_usage_str +) + + +def handle_set_build_volume(goptions, session, args): + "[admin] Move a build to a different volume" + usage = "usage: %prog set-build-volume [ ...]" + parser = OptionParser(usage=get_usage_str(usage)) + parser.add_option("-v", "--verbose", action="store_true", help="Be verbose") + (options, args) = parser.parse_args(args) + if len(args) < 2: + parser.error("You must provide a volume and at least one build") + volinfo = session.getVolume(args[0]) + if not volinfo: + error("No such volume: %s" % args[0]) + activate_session(session, goptions) + builds = [] + for nvr in args[1:]: + binfo = session.getBuild(nvr) + if not binfo: + print("No such build: %s" % nvr) + elif binfo['volume_id'] == volinfo['id']: + print("Build %s already on volume %s" % (nvr, volinfo['name'])) + else: + builds.append(binfo) + if not builds: + error("No builds to move") + for binfo in builds: + session.changeBuildVolume(binfo['id'], volinfo['id']) + if options.verbose: + print("%s: %s -> %s" % (binfo['nvr'], binfo['volume_name'], volinfo['name'])) + + diff --git a/cli/koji_cli/commands/cmd_set_pkg_arches.py b/cli/koji_cli/commands/cmd_set_pkg_arches.py new file mode 100644 index 0000000..c149800 --- /dev/null +++ b/cli/koji_cli/commands/cmd_set_pkg_arches.py @@ -0,0 +1,29 @@ +from __future__ import absolute_import, division + +from optparse import OptionParser + + +import koji + +from koji_cli.lib import ( + activate_session, + get_usage_str +) + + +def handle_set_pkg_arches(goptions, session, args): + "[admin] Set the list of extra arches for a package" + usage = "usage: %prog set-pkg-arches [options] [ ...]" + parser = OptionParser(usage=get_usage_str(usage)) + parser.add_option("--force", action='store_true', help="Force operation") + (options, args) = parser.parse_args(args) + if len(args) < 3: + parser.error("Please specify an archlist, a tag, and at least one package") + activate_session(session, goptions) + arches = koji.parse_arches(args[0]) + tag = args[1] + with session.multicall(strict=True) as m: + for package in args[2:]: + m.packageListSetArches(tag, package, arches, force=options.force) + + diff --git a/cli/koji_cli/commands/cmd_set_pkg_owner.py b/cli/koji_cli/commands/cmd_set_pkg_owner.py new file mode 100644 index 0000000..d20890b --- /dev/null +++ b/cli/koji_cli/commands/cmd_set_pkg_owner.py @@ -0,0 +1,28 @@ +from __future__ import absolute_import, division + +from optparse import OptionParser + + + +from koji_cli.lib import ( + activate_session, + get_usage_str +) + + +def handle_set_pkg_owner(goptions, session, args): + "[admin] Set the owner for a package" + usage = "usage: %prog set-pkg-owner [options] [ ...]" + parser = OptionParser(usage=get_usage_str(usage)) + parser.add_option("--force", action='store_true', help="Force operation") + (options, args) = parser.parse_args(args) + if len(args) < 3: + parser.error("Please specify an owner, a tag, and at least one package") + activate_session(session, goptions) + owner = args[0] + tag = args[1] + with session.multicall(strict=True) as m: + for package in args[2:]: + m.packageListSetOwner(tag, package, owner, force=options.force) + + diff --git a/cli/koji_cli/commands/cmd_set_pkg_owner_global.py b/cli/koji_cli/commands/cmd_set_pkg_owner_global.py new file mode 100644 index 0000000..c5ec5d7 --- /dev/null +++ b/cli/koji_cli/commands/cmd_set_pkg_owner_global.py @@ -0,0 +1,70 @@ +from __future__ import absolute_import, division + +from optparse import OptionParser + + + +from koji_cli.lib import ( + activate_session, + error, + get_usage_str +) + + +def handle_set_pkg_owner_global(goptions, session, args): + "[admin] Set the owner for a package globally" + usage = "usage: %prog set-pkg-owner-global [options] [ ...]" + parser = OptionParser(usage=get_usage_str(usage)) + parser.add_option("--verbose", action='store_true', help="List changes") + parser.add_option("--test", action='store_true', help="Test mode") + parser.add_option("--old-user", "--from", action="store", + help="Only change ownership for packages belonging to this user") + (options, args) = parser.parse_args(args) + if options.old_user: + if len(args) < 1: + parser.error("Please specify an owner") + elif len(args) < 2: + parser.error("Please specify an owner and at least one package") + activate_session(session, goptions) + owner = args[0] + packages = args[1:] + user = session.getUser(owner) + if not user: + error("No such user: %s" % owner) + opts = {'with_dups': True} + old_user = None + if options.old_user: + old_user = session.getUser(options.old_user) + if not old_user: + error("No such user: %s" % options.old_user) + opts['userID'] = old_user['id'] + to_change = [] + for package in packages: + entries = session.listPackages(pkgID=package, **opts) + if not entries: + print("No data for package %s" % package) + continue + to_change.extend(entries) + if not packages and options.old_user: + entries = session.listPackages(**opts) + if not entries: + error("No data for user %s" % old_user['name']) + to_change.extend(entries) + for entry in to_change: + if user['id'] == entry['owner_id']: + if options.verbose: + print("Preserving owner=%s for package %s in tag %s" + % (user['name'], package, entry['tag_name'])) + else: + if options.test: + print("Would have changed owner for %s in tag %s: %s -> %s" + % (entry['package_name'], entry['tag_name'], entry['owner_name'], + user['name'])) + continue + if options.verbose: + print("Changing owner for %s in tag %s: %s -> %s" + % (entry['package_name'], entry['tag_name'], entry['owner_name'], + user['name'])) + session.packageListSetOwner(entry['tag_id'], entry['package_name'], user['id']) + + diff --git a/cli/koji_cli/commands/cmd_set_task_priority.py b/cli/koji_cli/commands/cmd_set_task_priority.py new file mode 100644 index 0000000..990afd1 --- /dev/null +++ b/cli/koji_cli/commands/cmd_set_task_priority.py @@ -0,0 +1,47 @@ +from __future__ import absolute_import, division + +from optparse import OptionParser + + +import koji + +from koji_cli.lib import ( + activate_session, + error, + get_usage_str, + warn +) + + +def handle_set_task_priority(goptions, session, args): + "[admin] Set task priority" + usage = "usage: %prog set-task-priority [options] --priority= " \ + "[ ...]" + parser = OptionParser(usage=get_usage_str(usage)) + parser.add_option("--priority", type="int", help="New priority") + parser.add_option("--recurse", action="store_true", default=False, + help="Change priority of child tasks as well") + (options, args) = parser.parse_args(args) + if len(args) == 0: + parser.error("You must specify at least one task id") + + if options.priority is None: + parser.error("You must specify --priority") + try: + tasks = [int(a) for a in args] + except ValueError: + parser.error("Task numbers must be integers") + + activate_session(session, goptions) + + if not session.hasPerm('admin'): + logged_user = session.getLoggedInUser() + error("admin permission required (logged in as %s)" % logged_user['name']) + + for task_id in tasks: + try: + session.setTaskPriority(task_id, options.priority, options.recurse) + except koji.GenericError: + warn("Can't update task priority on closed task: %s" % task_id) + + diff --git a/cli/koji_cli/commands/cmd_show_groups.py b/cli/koji_cli/commands/cmd_show_groups.py new file mode 100644 index 0000000..4039457 --- /dev/null +++ b/cli/koji_cli/commands/cmd_show_groups.py @@ -0,0 +1,43 @@ +from __future__ import absolute_import, division + +import pprint +from optparse import OptionParser + + +import koji + +from koji_cli.lib import ( + ensure_connection, + get_usage_str +) + + +def anon_handle_show_groups(goptions, session, args): + "[info] Show groups data for a tag" + usage = "usage: %prog show-groups [options] " + parser = OptionParser(usage=get_usage_str(usage)) + parser.add_option("--comps", action="store_true", help="Print in comps format") + parser.add_option("-x", "--expand", action="store_true", default=False, + help="Expand groups in comps format") + parser.add_option("--spec", action="store_true", help="Print build spec") + parser.add_option("--show-blocked", action="store_true", dest="incl_blocked", + help="Show blocked packages") + (options, args) = parser.parse_args(args) + if len(args) != 1: + parser.error("Incorrect number of arguments") + if options.incl_blocked and (options.comps or options.spec): + parser.error("--show-blocked doesn't make sense for comps/spec output") + ensure_connection(session, goptions) + tag = args[0] + callopts = {} + if options.incl_blocked: + callopts['incl_blocked'] = True + groups = session.getTagGroups(tag, **callopts) + if options.comps: + print(koji.generate_comps(groups, expand_groups=options.expand)) + elif options.spec: + print(koji.make_groups_spec(groups, name='buildgroups', buildgroup='build')) + else: + pprint.pprint(groups) + + diff --git a/cli/koji_cli/commands/cmd_spin_appliance.py b/cli/koji_cli/commands/cmd_spin_appliance.py new file mode 100644 index 0000000..2337f28 --- /dev/null +++ b/cli/koji_cli/commands/cmd_spin_appliance.py @@ -0,0 +1,57 @@ +from __future__ import absolute_import, division + +from optparse import OptionParser + + + +from koji_cli.lib import ( + get_usage_str +) + + +def handle_spin_appliance(options, session, args): + """[build] Create an appliance given a kickstart file""" + + # Usage & option parsing + usage = "usage: %prog spin-appliance [options] " \ + "" + parser = OptionParser(usage=get_usage_str(usage)) + parser.add_option("--wait", action="store_true", + help="Wait on the appliance creation, even if running in the background") + parser.add_option("--nowait", action="store_false", dest="wait", + help="Don't wait on appliance creation") + parser.add_option("--noprogress", action="store_true", + help="Do not display progress of the upload") + parser.add_option("--background", action="store_true", + help="Run the appliance creation task at a lower priority") + parser.add_option("--ksurl", metavar="SCMURL", + help="The URL to the SCM containing the kickstart file") + parser.add_option("--ksversion", metavar="VERSION", + help="The syntax version used in the kickstart file") + parser.add_option("--scratch", action="store_true", help="Create a scratch appliance") + parser.add_option("--repo", action="append", + help="Specify a repo that will override the repo used to install " + "RPMs in the appliance. May be used multiple times. The " + "build tag repo associated with the target is the default.") + parser.add_option("--release", help="Forcibly set the release field") + parser.add_option("--specfile", metavar="URL", + help="SCM URL to spec file fragment to use to generate wrapper RPMs") + parser.add_option("--skip-tag", action="store_true", help="Do not attempt to tag package") + parser.add_option("--vmem", metavar="VMEM", default=None, + help="Set the amount of virtual memory in the appliance in MB, " + "default is 512") + parser.add_option("--vcpu", metavar="VCPU", default=None, + help="Set the number of virtual cpus in the appliance, default is 1") + parser.add_option("--format", metavar="DISK_FORMAT", default='raw', + help="Disk format, default is raw. Other options are qcow, qcow2, and vmx.") + + (task_options, args) = parser.parse_args(args) + + # Make sure the target and kickstart is specified. + print('spin-appliance is deprecated and will be replaced with image-build') + if len(args) != 5: + parser.error("Five arguments are required: a name, a version, an architecture, " + "a build target, and a relative path to a kickstart file.") + return _build_image(options, task_options, session, args, 'appliance') + + diff --git a/cli/koji_cli/commands/cmd_spin_livecd.py b/cli/koji_cli/commands/cmd_spin_livecd.py new file mode 100644 index 0000000..ffc03d6 --- /dev/null +++ b/cli/koji_cli/commands/cmd_spin_livecd.py @@ -0,0 +1,54 @@ +from __future__ import absolute_import, division + +from optparse import OptionParser + + + +from koji_cli.lib import ( + get_usage_str +) + + +def handle_spin_livecd(options, session, args): + """[build] Create a live CD image given a kickstart file""" + + # Usage & option parsing. + usage = "usage: %prog spin-livecd [options] " + parser = OptionParser(usage=get_usage_str(usage)) + parser.add_option("--wait", action="store_true", + help="Wait on the livecd creation, even if running in the background") + parser.add_option("--nowait", action="store_false", dest="wait", + help="Don't wait on livecd creation") + parser.add_option("--noprogress", action="store_true", + help="Do not display progress of the upload") + parser.add_option("--background", action="store_true", + help="Run the livecd creation task at a lower priority") + parser.add_option("--ksurl", metavar="SCMURL", + help="The URL to the SCM containing the kickstart file") + parser.add_option("--ksversion", metavar="VERSION", + help="The syntax version used in the kickstart file") + parser.add_option("--scratch", action="store_true", + help="Create a scratch LiveCD image") + parser.add_option("--repo", action="append", + help="Specify a repo that will override the repo used to install " + "RPMs in the LiveCD. May be used multiple times. The " + "build tag repo associated with the target is the default.") + parser.add_option("--release", help="Forcibly set the release field") + parser.add_option("--volid", help="Set the volume id") + parser.add_option("--specfile", metavar="URL", + help="SCM URL to spec file fragment to use to generate wrapper RPMs") + parser.add_option("--skip-tag", action="store_true", + help="Do not attempt to tag package") + (task_options, args) = parser.parse_args(args) + + # Make sure the target and kickstart is specified. + print('spin-livecd is deprecated and will be replaced with spin-livemedia') + if len(args) != 5: + parser.error("Five arguments are required: a name, a version, an architecture, " + "a build target, and a relative path to a kickstart file.") + if task_options.volid is not None and len(task_options.volid) > 32: + parser.error('Volume ID has a maximum length of 32 characters') + return _build_image(options, task_options, session, args, 'livecd') + + +# This handler is for spinning livemedia images diff --git a/cli/koji_cli/commands/cmd_spin_livemedia.py b/cli/koji_cli/commands/cmd_spin_livemedia.py new file mode 100644 index 0000000..0f0e63e --- /dev/null +++ b/cli/koji_cli/commands/cmd_spin_livemedia.py @@ -0,0 +1,75 @@ +from __future__ import absolute_import, division + +from optparse import OptionParser + + + +from koji_cli.lib import ( + get_usage_str +) + + +def handle_spin_livemedia(options, session, args): + """[build] Create a livemedia image given a kickstart file""" + + # Usage & option parsing. + usage = "usage: %prog spin-livemedia [options] " \ + "" + parser = OptionParser(usage=get_usage_str(usage)) + parser.add_option("--wait", action="store_true", + help="Wait on the livemedia creation, even if running in the background") + parser.add_option("--nowait", action="store_false", dest="wait", + help="Don't wait on livemedia creation") + parser.add_option("--noprogress", action="store_true", + help="Do not display progress of the upload") + parser.add_option("--background", action="store_true", + help="Run the livemedia creation task at a lower priority") + parser.add_option("--ksurl", metavar="SCMURL", + help="The URL to the SCM containing the kickstart file") + parser.add_option("--install-tree-url", metavar="URL", + help="Provide the URL for the install tree") + parser.add_option("--ksversion", metavar="VERSION", + help="The syntax version used in the kickstart file") + parser.add_option("--scratch", action="store_true", + help="Create a scratch LiveMedia image") + parser.add_option("--repo", action="append", + help="Specify a repo that will override the repo used to install " + "RPMs in the LiveMedia. May be used multiple times. The " + "build tag repo associated with the target is the default.") + parser.add_option("--release", help="Forcibly set the release field") + parser.add_option("--volid", help="Set the volume id") + parser.add_option("--specfile", metavar="URL", + help="SCM URL to spec file fragment to use to generate wrapper RPMs") + parser.add_option("--skip-tag", action="store_true", help="Do not attempt to tag package") + parser.add_option("--can-fail", action="store", dest="optional_arches", + metavar="ARCH1,ARCH2,...", default="", + help="List of archs which are not blocking for build (separated by commas.") + parser.add_option('--lorax_dir', metavar='DIR', + help='The relative path to the lorax templates ' + 'directory within the checkout of "lorax_url".') + parser.add_option('--lorax_url', metavar='URL', + help='The URL to the SCM containing any custom lorax templates that are ' + 'to be used to override the default templates.') + parser.add_option('--nomacboot', action="store_true", + help="Pass the nomacboot option to livemedia-creator") + parser.add_option('--ksrepo', action="store_true", + help="Do not overwrite repos in the kickstart") + parser.add_option('--squashfs-only', action="store_true", + help="Use a plain squashfs filesystem.") + parser.add_option('--compress-arg', action="append", default=[], metavar="ARG OPT", + help="List of compressions.") + (task_options, args) = parser.parse_args(args) + + # Make sure the target and kickstart is specified. + if len(args) != 5: + parser.error("Five arguments are required: a name, a version, a build target, " + "an architecture, and a relative path to a kickstart file.") + if task_options.lorax_url is not None and task_options.lorax_dir is None: + parser.error('The "--lorax_url" option requires that "--lorax_dir" also be used.') + if task_options.volid is not None and len(task_options.volid) > 32: + parser.error('Volume ID has a maximum length of 32 characters') + return _build_image(options, task_options, session, args, 'livemedia') + + +# This handler is for spinning appliance images +# diff --git a/cli/koji_cli/commands/cmd_tag_build.py b/cli/koji_cli/commands/cmd_tag_build.py new file mode 100644 index 0000000..c888a00 --- /dev/null +++ b/cli/koji_cli/commands/cmd_tag_build.py @@ -0,0 +1,40 @@ +from __future__ import absolute_import, division + +from optparse import OptionParser + + + +from koji_cli.lib import ( + _running_in_bg, + activate_session, + get_usage_str, + watch_tasks +) + + +def handle_tag_build(opts, session, args): + "[bind] Apply a tag to one or more builds" + usage = "usage: %prog tag-build [options] [ ...]" + parser = OptionParser(usage=get_usage_str(usage)) + parser.add_option("--force", action="store_true", help="force operation") + parser.add_option("--wait", action="store_true", + help="Wait on task, even if running in the background") + parser.add_option("--nowait", action="store_false", dest="wait", help="Do not wait on task") + (options, args) = parser.parse_args(args) + if len(args) < 2: + parser.error( + "This command takes at least two arguments: a tag name/ID and one or more package " + "n-v-r's") + activate_session(session, opts) + tasks = [] + for pkg in args[1:]: + task_id = session.tagBuild(args[0], pkg, force=options.force) + # XXX - wait on task + tasks.append(task_id) + print("Created task %d" % task_id) + if options.wait or (options.wait is None and not _running_in_bg()): + session.logout() + return watch_tasks(session, tasks, quiet=opts.quiet, + poll_interval=opts.poll_interval, topurl=opts.topurl) + + diff --git a/cli/koji_cli/commands/cmd_taginfo.py b/cli/koji_cli/commands/cmd_taginfo.py new file mode 100644 index 0000000..8a9adab --- /dev/null +++ b/cli/koji_cli/commands/cmd_taginfo.py @@ -0,0 +1,119 @@ +from __future__ import absolute_import, division + +import pprint +import time +from optparse import OptionParser + + +import koji + +from koji_cli.lib import ( + ensure_connection, + format_inheritance_flags, + get_usage_str +) + + +def anon_handle_taginfo(goptions, session, args): + "[info] Print basic information about a tag" + usage = "usage: %prog taginfo [options] [ ...]" + parser = OptionParser(usage=get_usage_str(usage)) + parser.add_option("--event", type='int', metavar="EVENT#", help="query at event") + parser.add_option("--ts", type='int', metavar="TIMESTAMP", + help="query at last event before timestamp") + parser.add_option("--repo", type='int', metavar="REPO#", help="query at event for a repo") + (options, args) = parser.parse_args(args) + if len(args) < 1: + parser.error("Please specify a tag") + ensure_connection(session, goptions) + event = koji.util.eventFromOpts(session, options) + event_opts = {} + if event: + event['timestr'] = time.asctime(time.localtime(event['ts'])) + print("Querying at event %(id)i (%(timestr)s)" % event) + event_opts['event'] = event['id'] + perms = dict([(p['id'], p['name']) for p in session.getAllPerms()]) + + tags = [] + for tag in args: + info = session.getBuildConfig(tag, **event_opts) + if info is None: + try: + info = session.getBuildConfig(int(tag), **event_opts) + except ValueError: + info = None + if info is None: + parser.error('No such tag: %s' % tag) + tags.append(info) + + for n, info in enumerate(tags): + if n > 0: + print('') + print("Tag: %(name)s [%(id)d]" % info) + print("Arches: %(arches)s" % info) + group_list = sorted([x['name'] for x in session.getTagGroups(info['id'], **event_opts)]) + print("Groups: " + ', '.join(group_list)) + if info.get('locked'): + print('LOCKED') + if info.get('perm_id') is not None: + perm_id = info['perm_id'] + print("Required permission: %r" % perms.get(perm_id, perm_id)) + if session.mavenEnabled(): + print("Maven support?: %s" % (info['maven_support'] and 'yes' or 'no')) + print("Include all Maven archives?: %s" % + (info['maven_include_all'] and 'yes' or 'no')) + if 'extra' in info: + print("Tag options:") + for key in sorted(info['extra'].keys()): + line = " %s : %s" % (key, pprint.pformat(info['extra'][key])) + if key in info.get('config_inheritance', {}).get('extra', []): + line = "%-30s [%s]" % (line, info['config_inheritance']['extra'][key]['name']) + print(line) + dest_targets = session.getBuildTargets(destTagID=info['id'], **event_opts) + build_targets = session.getBuildTargets(buildTagID=info['id'], **event_opts) + repos = {} + if not event: + for target in dest_targets + build_targets: + if target['build_tag'] not in repos: + repo = session.getRepo(target['build_tag']) + if repo is None: + repos[target['build_tag']] = "no active repo" + else: + repos[target['build_tag']] = "repo#%(id)i: %(creation_time)s" % repo + if dest_targets: + print("Targets that build into this tag:") + for target in dest_targets: + if event: + print(" %s (%s)" % (target['name'], target['build_tag_name'])) + else: + print(" %s (%s, %s)" % + (target['name'], target['build_tag_name'], repos[target['build_tag']])) + if build_targets: + print("This tag is a buildroot for one or more targets") + if not event: + print("Current repo: %s" % repos[info['id']]) + print("Targets that build from this tag:") + for target in build_targets: + print(" %s" % target['name']) + external_repos = session.getTagExternalRepos(tag_info=info['id'], **event_opts) + if external_repos: + print("External repos:") + for rinfo in external_repos: + if 'arches' not in rinfo: + # older hubs will not return this field + rinfo['arches'] = '-' + elif not rinfo['arches']: + rinfo['arches'] = 'inherited from tag' + # TODO else intersection of arches? + print(" %(priority)3i %(external_repo_name)s " + "(%(url)s, merge mode: %(merge_mode)s), arches: %(arches)s" % rinfo) + print("Inheritance:") + for parent in session.getInheritanceData(info['id'], **event_opts): + parent['flags'] = format_inheritance_flags(parent) + print(" %(priority)-4d %(flags)s %(name)s [%(parent_id)s]" % parent) + if parent['maxdepth'] is not None: + print(" maxdepth: %(maxdepth)s" % parent) + if parent['pkg_filter']: + print(" package filter: %(pkg_filter)s" % parent) + + diff --git a/cli/koji_cli/commands/cmd_taskinfo.py b/cli/koji_cli/commands/cmd_taskinfo.py new file mode 100644 index 0000000..dcfb4cb --- /dev/null +++ b/cli/koji_cli/commands/cmd_taskinfo.py @@ -0,0 +1,30 @@ +from __future__ import absolute_import, division + +from optparse import OptionParser + + + +from koji_cli.lib import ( + ensure_connection, + get_usage_str +) + + +def anon_handle_taskinfo(goptions, session, args): + """[info] Show information about a task""" + usage = "usage: %prog taskinfo [options] [ ...]" + parser = OptionParser(usage=get_usage_str(usage)) + parser.add_option("-r", "--recurse", action="store_true", + help="Show children of this task as well") + parser.add_option("-v", "--verbose", action="store_true", help="Be verbose") + (options, args) = parser.parse_args(args) + if len(args) < 1: + parser.error("You must specify at least one task ID") + + ensure_connection(session, goptions) + + for arg in args: + task_id = int(arg) + _printTaskInfo(session, task_id, goptions.topdir, 0, options.recurse, options.verbose) + + diff --git a/cli/koji_cli/commands/cmd_unblock_group_pkg.py b/cli/koji_cli/commands/cmd_unblock_group_pkg.py new file mode 100644 index 0000000..5b56a2c --- /dev/null +++ b/cli/koji_cli/commands/cmd_unblock_group_pkg.py @@ -0,0 +1,26 @@ +from __future__ import absolute_import, division + +from optparse import OptionParser + + + +from koji_cli.lib import ( + activate_session, + get_usage_str +) + + +def handle_unblock_group_pkg(goptions, session, args): + "[admin] Unblock a package from a group's package listing" + usage = "usage: %prog unblock-group-pkg [options] [ ...]" + parser = OptionParser(usage=get_usage_str(usage)) + (options, args) = parser.parse_args(args) + if len(args) < 3: + parser.error("You must specify a tag name, group name, and one or more package names") + tag = args[0] + group = args[1] + activate_session(session, goptions) + for pkg in args[2:]: + session.groupPackageListUnblock(tag, group, pkg) + + diff --git a/cli/koji_cli/commands/cmd_unblock_group_req.py b/cli/koji_cli/commands/cmd_unblock_group_req.py new file mode 100644 index 0000000..8703cfd --- /dev/null +++ b/cli/koji_cli/commands/cmd_unblock_group_req.py @@ -0,0 +1,26 @@ +from __future__ import absolute_import, division + +from optparse import OptionParser + + + +from koji_cli.lib import ( + activate_session, + get_usage_str +) + + +def handle_unblock_group_req(goptions, session, args): + "[admin] Unblock a group's requirement listing" + usage = "usage: %prog unblock-group-req [options] " + parser = OptionParser(usage=get_usage_str(usage)) + (options, args) = parser.parse_args(args) + if len(args) != 3: + parser.error("You must specify a tag name and two group names") + tag = args[0] + group = args[1] + req = args[2] + activate_session(session, goptions) + session.groupReqListUnblock(tag, group, req) + + diff --git a/cli/koji_cli/commands/cmd_unblock_notification.py b/cli/koji_cli/commands/cmd_unblock_notification.py new file mode 100644 index 0000000..e344a1b --- /dev/null +++ b/cli/koji_cli/commands/cmd_unblock_notification.py @@ -0,0 +1,34 @@ +from __future__ import absolute_import, division + +from optparse import OptionParser + + + +from koji_cli.lib import ( + activate_session, + get_usage_str +) + + +def handle_unblock_notification(goptions, session, args): + "[monitor] Unblock user's notification" + usage = "usage: %prog unblock-notification [options] [ ...]" + parser = OptionParser(usage=get_usage_str(usage)) + (options, args) = parser.parse_args(args) + + activate_session(session, goptions) + + if len(args) < 1: + parser.error("At least one notification block id has to be specified") + + try: + n_ids = [int(x) for x in args] + except ValueError: + parser.error("All notification block ids has to be integers") + + for n_id in n_ids: + session.deleteNotificationBlock(n_id) + if not goptions.quiet: + print("Notification block %d successfully removed." % n_id) + + diff --git a/cli/koji_cli/commands/cmd_unblock_pkg.py b/cli/koji_cli/commands/cmd_unblock_pkg.py new file mode 100644 index 0000000..504293e --- /dev/null +++ b/cli/koji_cli/commands/cmd_unblock_pkg.py @@ -0,0 +1,26 @@ +from __future__ import absolute_import, division + +from optparse import OptionParser + + + +from koji_cli.lib import ( + activate_session, + get_usage_str +) + + +def handle_unblock_pkg(goptions, session, args): + "[admin] Unblock a package in the listing for tag" + usage = "usage: %prog unblock-pkg [options] [ ...]" + parser = OptionParser(usage=get_usage_str(usage)) + (options, args) = parser.parse_args(args) + if len(args) < 2: + parser.error("Please specify a tag and at least one package") + activate_session(session, goptions) + tag = args[0] + with session.multicall(strict=True) as m: + for package in args[1:]: + m.packageListUnblock(tag, package) + + diff --git a/cli/koji_cli/commands/cmd_unlock_tag.py b/cli/koji_cli/commands/cmd_unlock_tag.py new file mode 100644 index 0000000..da13ed3 --- /dev/null +++ b/cli/koji_cli/commands/cmd_unlock_tag.py @@ -0,0 +1,54 @@ +from __future__ import absolute_import, division + +import fnmatch +from optparse import OptionParser + + + +from koji_cli.lib import ( + activate_session, + get_usage_str +) + + +def handle_unlock_tag(goptions, session, args): + "[admin] Unlock a tag" + usage = "usage: %prog unlock-tag [options] [ ...]" + parser = OptionParser(usage=get_usage_str(usage)) + parser.add_option("--glob", action="store_true", help="Treat args as glob patterns") + parser.add_option("-n", "--test", action="store_true", help="Test mode") + (options, args) = parser.parse_args(args) + if len(args) < 1: + parser.error("Please specify a tag") + activate_session(session, goptions) + if options.glob: + selected = [] + for tag in session.listTags(): + for pattern in args: + if fnmatch.fnmatch(tag['name'], pattern): + selected.append(tag) + break + if not selected: + print("No tags matched") + else: + selected = [] + for name in args: + tag = session.getTag(name) + if tag is None: + parser.error("No such tag: %s" % name) + selected.append(tag) + for tag in selected: + opts = {} + if tag['locked']: + opts['locked'] = False + if tag['perm_id']: + opts['perm'] = None + if not opts: + print("Tag %(name)s: not locked" % tag) + continue + if options.test: + print("Tag %s: skipping changes: %r" % (tag['name'], opts)) + else: + session.editTag2(tag['id'], **opts) + + diff --git a/cli/koji_cli/commands/cmd_untag_build.py b/cli/koji_cli/commands/cmd_untag_build.py new file mode 100644 index 0000000..def3624 --- /dev/null +++ b/cli/koji_cli/commands/cmd_untag_build.py @@ -0,0 +1,113 @@ +from __future__ import absolute_import, division + +import itertools +from optparse import OptionParser + + +import koji + +from koji_cli.lib import ( + activate_session, + error, + get_usage_str, + warn +) + + +def handle_untag_build(goptions, session, args): + "[bind] Remove a tag from one or more builds" + usage = "usage: %prog untag-build [options] [ ...]" + parser = OptionParser(usage=get_usage_str(usage)) + parser.add_option("--all", action="store_true", + help="untag all versions of the package in this tag, pkg is package name") + parser.add_option("--non-latest", action="store_true", + help="untag all versions of the package in this tag except the latest, " + "pkg is package name") + parser.add_option("-n", "--test", action="store_true", help="test mode") + parser.add_option("-v", "--verbose", action="store_true", help="print details") + parser.add_option("--force", action="store_true", help="force operation") + (options, args) = parser.parse_args(args) + if options.non_latest and options.force: + if len(args) < 1: + parser.error("Please specify a tag") + elif len(args) < 2: + parser.error( + "This command takes at least two arguments: a tag name/ID and one or more package " + "n-v-r's or package names") + activate_session(session, goptions) + tag = session.getTag(args[0]) + if not tag: + parser.error("No such tag: %s" % args[0]) + if options.all: + result = [] + with session.multicall() as m: + result.extend([m.queryHistory(tag=args[0], package=pkg, active=True) + for pkg in args[1:]]) + builds = [] + for r in result: + builds.extend(r.result['tag_listing']) + elif options.non_latest: + if options.force and len(args) == 1: + tagged = session.queryHistory(tag=args[0], active=True)['tag_listing'] + tagged = sorted(tagged, key=lambda k: (k['create_event']), reverse=True) + else: + result = [] + with session.multicall() as m: + result.extend([m.queryHistory(tag=args[0], package=pkg, active=True) + for pkg in args[1:]]) + tagged = [] + for r in result: + tagged.extend(r.result['tag_listing']) + tagged = sorted(tagged, key=lambda k: (k['create_event']), reverse=True) + # listTagged orders entries latest first + seen_pkg = {} + builds = [] + for binfo in tagged: + if binfo['name'] not in seen_pkg: + # latest for this package + nvr = '%s-%s-%s' % (binfo['name'], binfo['version'], binfo['release']) + if options.verbose: + print("Leaving latest build for package %s: %s" % (binfo['name'], nvr)) + else: + builds.append(binfo) + seen_pkg[binfo['name']] = 1 + else: + # find all pkg's builds in tag + pkgs = set([koji.parse_NVR(nvr)['name'] for nvr in args[1:]]) + result = [] + with session.multicall() as m: + result.extend([m.queryHistory(tag=args[0], pkg=pkg, active=True) for pkg in pkgs]) + tagged = [] + for r in result: + tagged.append(r.result['tag_listing']) + # flatten + tagged = list(itertools.chain(*[t for t in tagged])) + idx = dict([('%s-%s-%s' % (b['name'], b['version'], b['release']), b) for b in tagged]) + + # check exact builds + builds = [] + for nvr in args[1:]: + binfo = idx.get(nvr) + if binfo: + builds.append(binfo) + else: + # not in tag, see if it even exists + binfo = session.getBuild(nvr) + if not binfo: + warn("No such build: %s" % nvr) + else: + warn("Build %s not in tag %s" % (nvr, tag['name'])) + if not options.force: + error() + builds.reverse() + with session.multicall(strict=True) as m: + for binfo in builds: + build_nvr = '%s-%s-%s' % (binfo['name'], binfo['version'], binfo['release']) + if options.test: + print("would have untagged %s" % build_nvr) + else: + if options.verbose: + print("untagging %s" % build_nvr) + m.untagBuild(tag['name'], build_nvr, force=options.force) + + diff --git a/cli/koji_cli/commands/cmd_userinfo.py b/cli/koji_cli/commands/cmd_userinfo.py new file mode 100644 index 0000000..8565b66 --- /dev/null +++ b/cli/koji_cli/commands/cmd_userinfo.py @@ -0,0 +1,67 @@ +from __future__ import absolute_import, division + +from optparse import OptionParser + +from six.moves import filter, zip + +import koji + +from koji_cli.lib import ( + ensure_connection, + get_usage_str, + warn +) + + +def anon_handle_userinfo(goptions, session, args): + """[admin] Show information about a user""" + usage = "usage: %prog userinfo [options] [ ...]" + parser = OptionParser(usage=get_usage_str(usage)) + (options, args) = parser.parse_args(args) + if len(args) < 1: + parser.error("You must specify at least one username") + + ensure_connection(session, goptions) + + with session.multicall() as m: + userinfos = [m.getUser(user) for user in args] + user_infos = [] + for username, userinfo in zip(args, userinfos): + if userinfo.result is None: + warn("No such user: %s\n" % username) + continue + user_infos.append(userinfo.result) + user_infos = list(filter(None, user_infos)) + + calls = [] + with session.multicall() as m: + for user in user_infos: + results = [] + if not user: + warn("No such user: %s\n" % user) + continue + results.append(m.getUserPerms(user['id'])) + results.append(m.listPackages(userID=user['id'], with_dups=True, + queryOpts={'countOnly': True})) + results.append(m.listTasks(opts={'owner': user['id'], 'parent': None}, + queryOpts={'countOnly': True})) + results.append(m.listBuilds(userID=user['id'], queryOpts={'countOnly': True})) + calls.append(results) + + for userinfo, (perms, pkgs, tasks, builds) in zip(user_infos, calls): + print("User name: %s" % userinfo['name']) + print("User ID: %d" % userinfo['id']) + if 'krb_principals' in userinfo: + print("krb principals:") + for krb in userinfo['krb_principals']: + print(" %s" % krb) + if perms.result: + print("Permissions:") + for perm in perms.result: + print(" %s" % perm) + print("Status: %s" % koji.USER_STATUS[userinfo['status']]) + print("Usertype: %s" % koji.USERTYPES[userinfo['usertype']]) + print("Number of packages: %d" % pkgs.result) + print("Number of tasks: %d" % tasks.result) + print("Number of builds: %d" % builds.result) + print('') diff --git a/cli/koji_cli/commands/cmd_version.py b/cli/koji_cli/commands/cmd_version.py new file mode 100644 index 0000000..926a5f5 --- /dev/null +++ b/cli/koji_cli/commands/cmd_version.py @@ -0,0 +1,22 @@ +from __future__ import absolute_import, division + + + +import koji + +from koji_cli.lib import ( + ensure_connection +) + + +def handle_version(goptions, session, args): + """Report client and hub versions""" + ensure_connection(session, goptions) + print('Client: %s' % koji.__version__) + try: + version = session.getKojiVersion() + print("Hub: %s" % version) + except koji.GenericError: + print("Hub: Can't determine (older than 1.23)") + + diff --git a/cli/koji_cli/commands/cmd_wait_repo.py b/cli/koji_cli/commands/cmd_wait_repo.py new file mode 100644 index 0000000..d4b4784 --- /dev/null +++ b/cli/koji_cli/commands/cmd_wait_repo.py @@ -0,0 +1,110 @@ +from __future__ import absolute_import, division + +import time +from optparse import OptionParser + + +import koji + +from koji_cli.lib import ( + ensure_connection, + error, + get_usage_str, + warn +) + + +def anon_handle_wait_repo(options, session, args): + "[monitor] Wait for a repo to be regenerated" + usage = "usage: %prog wait-repo [options] " + parser = OptionParser(usage=get_usage_str(usage)) + parser.add_option("--build", metavar="NVR", dest="builds", action="append", default=[], + help="Check that the given build is in the newly-generated repo " + "(may be used multiple times)") + parser.add_option("--target", action="store_true", + help="Interpret the argument as a build target name") + parser.add_option("--timeout", type="int", default=120, + help="Amount of time to wait (in minutes) before giving up " + "(default: 120)") + parser.add_option("--quiet", action="store_true", default=options.quiet, + help="Suppress output, success or failure will be indicated by the return " + "value only") + (suboptions, args) = parser.parse_args(args) + + start = time.time() + + builds = [koji.parse_NVR(build) for build in suboptions.builds] + if len(args) < 1: + parser.error("Please specify a tag name") + elif len(args) > 1: + parser.error("Only one tag may be specified") + + tag = args[0] + + ensure_connection(session, options) + if suboptions.target: + target_info = session.getBuildTarget(tag) + if not target_info: + parser.error("No such build target: %s" % tag) + tag = target_info['build_tag_name'] + tag_id = target_info['build_tag'] + else: + tag_info = session.getTag(tag) + if not tag_info: + parser.error("No such tag: %s" % tag) + targets = session.getBuildTargets(buildTagID=tag_info['id']) + if not targets: + warn("%(name)s is not a build tag for any target" % tag_info) + targets = session.getBuildTargets(destTagID=tag_info['id']) + if targets: + maybe = {}.fromkeys([t['build_tag_name'] for t in targets]) + maybe = sorted(maybe.keys()) + warn("Suggested tags: %s" % ', '.join(maybe)) + error() + tag_id = tag_info['id'] + + for nvr in builds: + data = session.getLatestBuilds(tag_id, package=nvr["name"]) + if len(data) == 0: + warn("Package %s is not in tag %s" % (nvr["name"], tag)) + else: + present_nvr = [x["nvr"] for x in data][0] + expected_nvr = '%(name)s-%(version)s-%(release)s' % nvr + if present_nvr != expected_nvr: + warn("nvr %s is not current in tag %s\n latest build in %s is %s" % + (expected_nvr, tag, tag, present_nvr)) + + last_repo = None + repo = session.getRepo(tag_id) + + while True: + if builds and repo and repo != last_repo: + if koji.util.checkForBuilds(session, tag_id, builds, repo['create_event'], + latest=True): + if not suboptions.quiet: + print("Successfully waited %s for %s to appear in the %s repo" % + (koji.util.duration(start), koji.util.printList(suboptions.builds), tag)) + return + + if (time.time() - start) >= (suboptions.timeout * 60.0): + if not suboptions.quiet: + if builds: + error("Unsuccessfully waited %s for %s to appear in the %s repo" % + (koji.util.duration(start), koji.util.printList(suboptions.builds), tag)) + else: + error("Unsuccessfully waited %s for a new %s repo" % + (koji.util.duration(start), tag)) + error() + + time.sleep(options.poll_interval) + last_repo = repo + repo = session.getRepo(tag_id) + + if not builds: + if repo != last_repo: + if not suboptions.quiet: + print("Successfully waited %s for a new %s repo" % + (koji.util.duration(start), tag)) + return + + diff --git a/cli/koji_cli/commands/cmd_watch_logs.py b/cli/koji_cli/commands/cmd_watch_logs.py new file mode 100644 index 0000000..10189a4 --- /dev/null +++ b/cli/koji_cli/commands/cmd_watch_logs.py @@ -0,0 +1,49 @@ +from __future__ import absolute_import, division + +from optparse import OptionParser + + + +from koji_cli.lib import ( + _list_tasks, + activate_session, + ensure_connection, + get_usage_str, + watch_logs +) + + +def anon_handle_watch_logs(goptions, session, args): + "[monitor] Watch logs in realtime" + usage = "usage: %prog watch-logs [options] [ ...]" + parser = OptionParser(usage=get_usage_str(usage)) + parser.add_option("--log", help="Watch only a specific log") + parser.add_option("--mine", action="store_true", + help="Watch logs for all your tasks, task_id arguments are forbidden in " + "this case.") + parser.add_option("--follow", action="store_true", help="Follow spawned child tasks") + (options, args) = parser.parse_args(args) + + if options.mine: + activate_session(session, goptions) + if args: + parser.error("Selection options cannot be combined with a task list") + tasks = _list_tasks(options, session) + tasks = [t['id'] for t in tasks] + if not tasks: + print("You've no active tasks.") + return + else: + ensure_connection(session, goptions) + tasks = [] + for task in args: + try: + tasks.append(int(task)) + except ValueError: + parser.error("task id must be an integer") + if not tasks: + parser.error("at least one task id must be specified") + + watch_logs(session, tasks, options, goptions.poll_interval) + + diff --git a/cli/koji_cli/commands/cmd_watch_task.py b/cli/koji_cli/commands/cmd_watch_task.py new file mode 100644 index 0000000..ae6ea81 --- /dev/null +++ b/cli/koji_cli/commands/cmd_watch_task.py @@ -0,0 +1,60 @@ +from __future__ import absolute_import, division + +from optparse import OptionParser + + + +from koji_cli.lib import ( + _list_tasks, + activate_session, + ensure_connection, + get_usage_str, + watch_tasks +) + + +def anon_handle_watch_task(goptions, session, args): + "[monitor] Track progress of particular tasks" + usage = "usage: %prog watch-task [options] [ ...]" + parser = OptionParser(usage=get_usage_str(usage)) + parser.add_option("--quiet", action="store_true", default=goptions.quiet, + help="Do not print the task information") + parser.add_option("--mine", action="store_true", help="Just watch your tasks") + parser.add_option("--user", help="Only tasks for this user") + parser.add_option("--arch", help="Only tasks for this architecture") + parser.add_option("--method", help="Only tasks of this method") + parser.add_option("--channel", help="Only tasks in this channel") + parser.add_option("--host", help="Only tasks for this host") + (options, args) = parser.parse_args(args) + selection = (options.mine or + options.user or + options.arch or + options.method or + options.channel or + options.host) + if args and selection: + parser.error("Selection options cannot be combined with a task list") + + if options.mine: + activate_session(session, goptions) + else: + ensure_connection(session, goptions) + if selection: + tasks = [task['id'] for task in _list_tasks(options, session)] + if not tasks: + print("(no tasks)") + return + else: + tasks = [] + for task in args: + try: + tasks.append(int(task)) + except ValueError: + parser.error("task id must be an integer") + if not tasks: + parser.error("at least one task id must be specified") + + return watch_tasks(session, tasks, quiet=options.quiet, + poll_interval=goptions.poll_interval, topurl=goptions.topurl) + + diff --git a/cli/koji_cli/commands/cmd_win_build.py b/cli/koji_cli/commands/cmd_win_build.py new file mode 100644 index 0000000..661c164 --- /dev/null +++ b/cli/koji_cli/commands/cmd_win_build.py @@ -0,0 +1,87 @@ +from __future__ import absolute_import, division + +from optparse import OptionParser + + + +from koji_cli.lib import ( + _running_in_bg, + activate_session, + get_usage_str, + watch_tasks +) + + +def handle_win_build(options, session, args): + """[build] Build a Windows package from source""" + # Usage & option parsing + usage = "usage: %prog win-build [options] " + parser = OptionParser(usage=get_usage_str(usage)) + parser.add_option("--winspec", metavar="URL", + help="SCM URL to retrieve the build descriptor from. " + "If not specified, the winspec must be in the root directory " + "of the source repository.") + parser.add_option("--patches", metavar="URL", + help="SCM URL of a directory containing patches to apply " + "to the sources before building") + parser.add_option("--cpus", type="int", + help="Number of cpus to allocate to the build VM " + "(requires admin access)") + parser.add_option("--mem", type="int", + help="Amount of memory (in megabytes) to allocate to the build VM " + "(requires admin access)") + parser.add_option("--static-mac", action="store_true", + help="Retain the original MAC address when cloning the VM") + parser.add_option("--specfile", metavar="URL", + help="SCM URL of a spec file fragment to use to generate wrapper RPMs") + parser.add_option("--scratch", action="store_true", + help="Perform a scratch build") + parser.add_option("--repo-id", type="int", help="Use a specific repo") + parser.add_option("--skip-tag", action="store_true", help="Do not attempt to tag package") + parser.add_option("--background", action="store_true", + help="Run the build at a lower priority") + parser.add_option("--wait", action="store_true", + help="Wait on the build, even if running in the background") + parser.add_option("--nowait", action="store_false", dest="wait", help="Don't wait on build") + parser.add_option("--quiet", action="store_true", + help="Do not print the task information", default=options.quiet) + (build_opts, args) = parser.parse_args(args) + if len(args) != 3: + parser.error( + "Exactly three arguments (a build target, a SCM URL, and a VM name) are required") + activate_session(session, options) + target = args[0] + if target.lower() == "none" and build_opts.repo_id: + target = None + build_opts.skip_tag = True + else: + build_target = session.getBuildTarget(target) + if not build_target: + parser.error("No such build target: %s" % target) + dest_tag = session.getTag(build_target['dest_tag']) + if not dest_tag: + parser.error("No such destination tag: %s" % build_target['dest_tag_name']) + if dest_tag['locked'] and not build_opts.scratch: + parser.error("Destination tag %s is locked" % dest_tag['name']) + scmurl = args[1] + vm_name = args[2] + opts = {} + for key in ('winspec', 'patches', 'cpus', 'mem', 'static_mac', + 'specfile', 'scratch', 'repo_id', 'skip_tag'): + val = getattr(build_opts, key) + if val is not None: + opts[key] = val + priority = None + if build_opts.background: + # relative to koji.PRIO_DEFAULT + priority = 5 + task_id = session.winBuild(vm_name, scmurl, target, opts, priority=priority) + if not build_opts.quiet: + print("Created task: %d" % task_id) + print("Task info: %s/taskinfo?taskID=%s" % (options.weburl, task_id)) + if build_opts.wait or (build_opts.wait is None and not _running_in_bg()): + session.logout() + return watch_tasks(session, [task_id], quiet=build_opts.quiet, + poll_interval=options.poll_interval, topurl=options.topurl) + + diff --git a/cli/koji_cli/commands/cmd_wrapper_rpm.py b/cli/koji_cli/commands/cmd_wrapper_rpm.py new file mode 100644 index 0000000..acdb8d6 --- /dev/null +++ b/cli/koji_cli/commands/cmd_wrapper_rpm.py @@ -0,0 +1,87 @@ +from __future__ import absolute_import, division + +from optparse import OptionParser + + +import koji +from koji.util import to_list + +from koji_cli.lib import ( + _running_in_bg, + activate_session, + get_usage_str, + watch_tasks +) + + +def handle_wrapper_rpm(options, session, args): + """[build] Build wrapper rpms for any archives associated with a build.""" + usage = "usage: %prog wrapper-rpm [options] " + parser = OptionParser(usage=get_usage_str(usage)) + parser.add_option("--create-build", action="store_true", + help="Create a new build to contain wrapper rpms") + parser.add_option("--ini", action="append", dest="inis", metavar="CONFIG", default=[], + help="Pass build parameters via a .ini file") + parser.add_option("-s", "--section", + help="Get build parameters from this section of the .ini") + parser.add_option("--skip-tag", action="store_true", + help="If creating a new build, don't tag it") + parser.add_option("--scratch", action="store_true", help="Perform a scratch build") + parser.add_option("--wait", action="store_true", + help="Wait on build, even if running in the background") + parser.add_option("--nowait", action="store_false", dest="wait", help="Don't wait on build") + parser.add_option("--background", action="store_true", + help="Run the build at a lower priority") + + (build_opts, args) = parser.parse_args(args) + if build_opts.inis: + if len(args) != 1: + parser.error("Exactly one argument (a build target) is required") + else: + if len(args) < 3: + parser.error("You must provide a build target, a build ID or NVR, " + "and a SCM URL to a specfile fragment") + activate_session(session, options) + + target = args[0] + if build_opts.inis: + try: + params = koji.util.parse_maven_param(build_opts.inis, scratch=build_opts.scratch, + section=build_opts.section) + except ValueError as e: + parser.error(e.args[0]) + opts = to_list(params.values())[0] + if opts.get('type') != 'wrapper': + parser.error("Section %s does not contain a wrapper-rpm config" % + to_list(params.keys())[0]) + url = opts['scmurl'] + package = opts['buildrequires'][0] + target_info = session.getBuildTarget(target, strict=True) + latest_builds = session.getLatestBuilds(target_info['dest_tag'], package=package) + if not latest_builds: + parser.error("No build of %s in %s" % (package, target_info['dest_tag_name'])) + build_id = latest_builds[0]['nvr'] + else: + build_id = args[1] + if build_id.isdigit(): + build_id = int(build_id) + url = args[2] + priority = None + if build_opts.background: + priority = 5 + opts = {} + if build_opts.create_build: + opts['create_build'] = True + if build_opts.skip_tag: + opts['skip_tag'] = True + if build_opts.scratch: + opts['scratch'] = True + task_id = session.wrapperRPM(build_id, url, target, priority, opts=opts) + print("Created task: %d" % task_id) + print("Task info: %s/taskinfo?taskID=%s" % (options.weburl, task_id)) + if build_opts.wait or (build_opts.wait is None and not _running_in_bg()): + session.logout() + return watch_tasks(session, [task_id], quiet=options.quiet, + poll_interval=options.poll_interval, topurl=options.topurl) + + diff --git a/cli/koji_cli/commands/cmd_write_signed_rpm.py b/cli/koji_cli/commands/cmd_write_signed_rpm.py new file mode 100644 index 0000000..3367586 --- /dev/null +++ b/cli/koji_cli/commands/cmd_write_signed_rpm.py @@ -0,0 +1,69 @@ +from __future__ import absolute_import, division + +from optparse import OptionParser + +from six.moves import zip + +import koji + +from koji_cli.lib import ( + activate_session, + get_usage_str +) + + +def handle_write_signed_rpm(goptions, session, args): + "[admin] Write signed RPMs to disk" + usage = "usage: %prog write-signed-rpm [options] [ ...]" + parser = OptionParser(usage=get_usage_str(usage)) + parser.add_option("--all", action="store_true", help="Write out all RPMs signed with this key") + parser.add_option("--buildid", help="Specify a build id rather than an n-v-r") + (options, args) = parser.parse_args(args) + if len(args) < 1: + parser.error("A signature key must be specified") + if len(args) < 2 and not (options.all or options.buildid): + parser.error("At least one RPM must be specified") + key = args.pop(0).lower() + activate_session(session, goptions) + if options.all: + rpms = session.queryRPMSigs(sigkey=key) + with session.multicall() as m: + results = [m.getRPM(r['rpm_id']) for r in rpms] + rpms = [x.result for x in results] + elif options.buildid: + rpms = session.listRPMs(int(options.buildid)) + else: + nvrs = [] + rpms = [] + + with session.multicall() as m: + result = [m.getRPM(nvra, strict=False) for nvra in args] + for rpm, nvra in zip(result, args): + rpm = rpm.result + if rpm: + rpms.append(rpm) + else: + nvrs.append(nvra) + + # for historical reasons, we also accept nvrs + with session.multicall() as m: + result = [m.getBuild(nvr, strict=True) for nvr in nvrs] + builds = [] + for nvr, build in zip(nvrs, result): + try: + builds.append(build.result['id']) + except koji.GenericError: + raise koji.GenericError("No such rpm or build: %s" % nvr) + + with session.multicall() as m: + rpm_lists = [m.listRPMs(buildID=build_id) for build_id in builds] + for rpm_list in rpm_lists: + rpms.extend(rpm_list.result) + + with session.multicall(strict=True) as m: + for i, rpminfo in enumerate(rpms): + nvra = "%(name)s-%(version)s-%(release)s.%(arch)s" % rpminfo + print("[%d/%d] %s" % (i + 1, len(rpms), nvra)) + m.writeSignedRPM(rpminfo['id'], key) + + diff --git a/cli/koji_cli/split.py b/cli/koji_cli/split.py index 50b44f8..55520f7 100755 --- a/cli/koji_cli/split.py +++ b/cli/koji_cli/split.py @@ -57,6 +57,29 @@ from koji_cli.lib import ( watch_tasks, truncate_string ) + + +""" + +INIT = """import importlib +import inspect +import pkgutil +import six + +__all__ = [] + +for loader, name, is_pkg in pkgutil.walk_packages(__path__): + if six.PY2: + module = loader.find_module(name).load_module(name) + else: + spec = loader.find_spec(name) + module = importlib.util.module_from_spec(spec) + spec.loader.exec_module(module) + + for name, entry in inspect.getmembers(module): + if name.startswith('handle_') or name.startswith('anon_handle_'): + globals()[name] = entry + __all__.append(name) """ try: @@ -65,7 +88,9 @@ except: pass os.mkdir('commands') -init = open('commands/__init__.py', 'wt') +with open('commands/__init__.py', 'wt') as f: + f.write(INIT) + out = open('xyz.py', 'wt') out_cmd = None @@ -82,7 +107,6 @@ for line in open('c.py'): out_cmd = open(os.path.join('commands', "%s.py" % fn), 'wt') out_cmd.write(HEAD) out_cmd.write(line) - init.write('from .%s import %s\n' % (fn, fname)) state = 'in' else: out.write(line) @@ -99,9 +123,10 @@ for line in open('c.py'): out_cmd = open(os.path.join('commands', "%s.py" % fn), 'wt') out_cmd.write(HEAD) out_cmd.write(line) - init.write('from .%s import %s\n' % (fn, fname)) state = 'in' else: out_cmd.write(line) else: out_cmd.write(line) + +# run autoflake -i -r --remove-all-unused-imports to clean up headers From 6f66b37f662f125b5532d23a87aece6877eeb5c9 Mon Sep 17 00:00:00 2001 From: Tester Date: Jun 13 2022 14:57:17 +0000 Subject: [PATCH 4/6] manual fixes after script run --- diff --git a/cli/koji_cli/commands/add_channel.py b/cli/koji_cli/commands/add_channel.py new file mode 100644 index 0000000..9d78027 --- /dev/null +++ b/cli/koji_cli/commands/add_channel.py @@ -0,0 +1,35 @@ +from __future__ import absolute_import, division + +from optparse import OptionParser +import koji + +from koji_cli.lib import ( + activate_session, + error, + get_usage_str +) + + +def handle_add_channel(goptions, session, args): + "[admin] Add a channel" + usage = "usage: %prog add-channel [options] " + parser = OptionParser(usage=get_usage_str(usage)) + parser.add_option("--description", help="Description of channel") + (options, args) = parser.parse_args(args) + if len(args) != 1: + parser.error("Please specify one channel name") + activate_session(session, goptions) + channel_name = args[0] + try: + channel_id = session.addChannel(channel_name, description=options.description) + except koji.GenericError as ex: + msg = str(ex) + if 'channel %s already exists' % channel_name in msg: + error("channel %s already exists" % channel_name) + elif 'Invalid method:' in msg: + version = session.getKojiVersion() + error("addChannel is available on hub from Koji 1.26 version, your version is %s" % + version) + else: + error(msg) + print("%s added: id %d" % (args[0], channel_id)) diff --git a/cli/koji_cli/commands/add_external_repo.py b/cli/koji_cli/commands/add_external_repo.py new file mode 100644 index 0000000..40d28be --- /dev/null +++ b/cli/koji_cli/commands/add_external_repo.py @@ -0,0 +1,84 @@ +from __future__ import absolute_import, division + +from optparse import OptionParser + +import koji + +from koji_cli.lib import ( + activate_session, + get_usage_str +) + + +def _pick_external_repo_priority(session, tag): + """pick priority after current ones, leaving space for later insertions""" + repolist = session.getTagExternalRepos(tag_info=tag) + # ordered by priority + if not repolist: + priority = 5 + else: + priority = (repolist[-1]['priority'] + 7) // 5 * 5 + # at least 3 higher than current max and a multiple of 5 + return priority + + +def _parse_tagpri(tagpri): + parts = tagpri.rsplit('::', 1) + tag = parts[0] + if len(parts) == 1: + return tag, None + elif parts[1] in ('auto', '-1'): + return tag, None + else: + try: + pri = int(parts[1]) + except ValueError: + raise koji.GenericError("Invalid priority: %s" % parts[1]) + return tag, pri + + +def handle_add_external_repo(goptions, session, args): + "[admin] Create an external repo and/or add one to a tag" + usage = "usage: %prog add-external-repo [options] []" + parser = OptionParser(usage=get_usage_str(usage)) + parser.add_option("-t", "--tag", action="append", metavar="TAG", + help="Also add repo to tag. Use tag::N to set priority") + parser.add_option("-p", "--priority", type='int', + help="Set priority (when adding to tag)") + parser.add_option("-m", "--mode", help="Set merge mode") + parser.add_option("-a", "--arches", metavar="ARCH1,ARCH2, ...", + help="Use only subset of arches from given repo") + (options, args) = parser.parse_args(args) + activate_session(session, goptions) + if options.mode: + if options.mode not in koji.REPO_MERGE_MODES: + parser.error('Invalid mode: %s' % options.mode) + if not options.tag: + parser.error('The --mode option can only be used with --tag') + if len(args) == 1: + name = args[0] + rinfo = session.getExternalRepo(name, strict=True) + if not options.tag: + parser.error("A url is required to create an external repo entry") + elif len(args) == 2: + name, url = args + rinfo = session.createExternalRepo(name, url) + print("Created external repo %(id)i" % rinfo) + else: + parser.error("Incorrect number of arguments") + if options.tag: + for tagpri in options.tag: + tag, priority = _parse_tagpri(tagpri) + if priority is None: + if options.priority is not None: + priority = options.priority + else: + priority = _pick_external_repo_priority(session, tag) + callopts = {} + if options.mode: + callopts['merge_mode'] = options.mode + if options.arches: + callopts['arches'] = options.arches + session.addExternalRepoToTag(tag, rinfo['name'], priority, **callopts) + print("Added external repo %s to tag %s (priority %i)" + % (rinfo['name'], tag, priority)) diff --git a/cli/koji_cli/commands/add_group.py b/cli/koji_cli/commands/add_group.py new file mode 100644 index 0000000..b9277cd --- /dev/null +++ b/cli/koji_cli/commands/add_group.py @@ -0,0 +1,35 @@ +from __future__ import absolute_import, division + +from optparse import OptionParser + +from koji_cli.lib import ( + activate_session, + error, + get_usage_str +) + + +def handle_add_group(goptions, session, args): + "[admin] Add a group to a tag" + usage = "usage: %prog add-group " + parser = OptionParser(usage=get_usage_str(usage)) + (options, args) = parser.parse_args(args) + if len(args) != 2: + parser.error("Please specify a tag name and a group name") + tag = args[0] + group = args[1] + + activate_session(session, goptions) + if not (session.hasPerm('admin') or session.hasPerm('tag')): + parser.error("This action requires tag or admin privileges") + + dsttag = session.getTag(tag) + if not dsttag: + error("No such tag: %s" % tag) + + groups = dict([(p['name'], p['group_id']) for p in session.getTagGroups(tag, inherit=False)]) + group_id = groups.get(group, None) + if group_id is not None: + error("Group %s already exists for tag %s" % (group, tag)) + + session.groupListAdd(tag, group) diff --git a/cli/koji_cli/commands/add_group_pkg.py b/cli/koji_cli/commands/add_group_pkg.py new file mode 100644 index 0000000..5473b99 --- /dev/null +++ b/cli/koji_cli/commands/add_group_pkg.py @@ -0,0 +1,22 @@ +from __future__ import absolute_import, division + +from optparse import OptionParser + +from koji_cli.lib import ( + activate_session, + get_usage_str +) + + +def handle_add_group_pkg(goptions, session, args): + "[admin] Add a package to a group's package listing" + usage = "usage: %prog add-group-pkg [options] [ ...]" + parser = OptionParser(usage=get_usage_str(usage)) + (options, args) = parser.parse_args(args) + if len(args) < 3: + parser.error("You must specify a tag name, group name, and one or more package names") + tag = args[0] + group = args[1] + activate_session(session, goptions) + for pkg in args[2:]: + session.groupPackageListAdd(tag, group, pkg) diff --git a/cli/koji_cli/commands/add_group_req.py b/cli/koji_cli/commands/add_group_req.py new file mode 100644 index 0000000..5061ade --- /dev/null +++ b/cli/koji_cli/commands/add_group_req.py @@ -0,0 +1,22 @@ +from __future__ import absolute_import, division + +from optparse import OptionParser + +from koji_cli.lib import ( + activate_session, + get_usage_str +) + + +def handle_add_group_req(goptions, session, args): + "[admin] Add a group to a group's required list" + usage = "usage: %prog add-group-req [options] " + parser = OptionParser(usage=get_usage_str(usage)) + (options, args) = parser.parse_args(args) + if len(args) != 3: + parser.error("You must specify a tag name and two group names") + tag = args[0] + group = args[1] + req = args[2] + activate_session(session, goptions) + session.groupReqListAdd(tag, group, req) diff --git a/cli/koji_cli/commands/add_host.py b/cli/koji_cli/commands/add_host.py new file mode 100644 index 0000000..7fb58a0 --- /dev/null +++ b/cli/koji_cli/commands/add_host.py @@ -0,0 +1,33 @@ +from __future__ import absolute_import, division + +from optparse import OptionParser + +from koji_cli.lib import ( + activate_session, + error, + get_usage_str +) + + +def handle_add_host(goptions, session, args): + "[admin] Add a host" + usage = "usage: %prog add-host [options] [ ...]" + parser = OptionParser(usage=get_usage_str(usage)) + parser.add_option("--krb-principal", + help="set a non-default kerberos principal for the host") + parser.add_option("--force", default=False, action="store_true", + help="if existing used is a regular user, convert it to a host") + (options, args) = parser.parse_args(args) + if len(args) < 2: + parser.error("Please specify a hostname and at least one arch") + host = args[0] + activate_session(session, goptions) + id = session.getHost(host) + if id: + error("%s is already in the database" % host) + else: + kwargs = {'force': options.force} + if options.krb_principal is not None: + kwargs['krb_principal'] = options.krb_principal + id = session.addHost(host, args[1:], **kwargs) + print("%s added: id %d" % (host, id)) diff --git a/cli/koji_cli/commands/add_host_to_channel.py b/cli/koji_cli/commands/add_host_to_channel.py new file mode 100644 index 0000000..a797f30 --- /dev/null +++ b/cli/koji_cli/commands/add_host_to_channel.py @@ -0,0 +1,42 @@ +from __future__ import absolute_import, division + +from optparse import SUPPRESS_HELP, OptionParser + +from koji_cli.lib import ( + activate_session, + error, + get_usage_str +) + + +def handle_add_host_to_channel(goptions, session, args): + "[admin] Add a host to a channel" + usage = "usage: %prog add-host-to-channel [options] " + parser = OptionParser(usage=get_usage_str(usage)) + parser.add_option("--list", action="store_true", help=SUPPRESS_HELP) + parser.add_option("--new", action="store_true", help="Create channel if needed") + parser.add_option("--force", action="store_true", help="force added, if possible") + (options, args) = parser.parse_args(args) + if not options.list and len(args) != 2: + parser.error("Please specify a hostname and a channel") + activate_session(session, goptions) + if options.list: + for channel in session.listChannels(): + print(channel['name']) + return + channel = args[1] + if not options.new: + channelinfo = session.getChannel(channel) + if not channelinfo: + error("No such channel: %s" % channel) + + host = args[0] + hostinfo = session.getHost(host) + if not hostinfo: + error("No such host: %s" % host) + kwargs = {} + if options.new: + kwargs['create'] = True + if options.force: + kwargs['force'] = True + session.addHostToChannel(host, channel, **kwargs) diff --git a/cli/koji_cli/commands/add_notification.py b/cli/koji_cli/commands/add_notification.py new file mode 100644 index 0000000..769dbe7 --- /dev/null +++ b/cli/koji_cli/commands/add_notification.py @@ -0,0 +1,54 @@ +from __future__ import absolute_import, division + +from optparse import OptionParser + +import koji + +from koji_cli.lib import ( + activate_session, + get_usage_str +) + + +def handle_add_notification(goptions, session, args): + "[monitor] Add user's notification" + usage = "usage: %prog add-notification [options]" + parser = OptionParser(usage=get_usage_str(usage)) + parser.add_option("--user", help="Add notifications for this user (admin-only)") + parser.add_option("--package", help="Add notifications for this package") + parser.add_option("--tag", help="Add notifications for this tag") + parser.add_option("--success-only", action="store_true", default=False, help="") + (options, args) = parser.parse_args(args) + + if len(args) != 0: + parser.error("This command takes no arguments") + + if not options.package and not options.tag: + parser.error("Command need at least one from --tag or --package options.") + + activate_session(session, goptions) + + if options.user and not session.hasPerm('admin'): + parser.error("--user requires admin permission") + + if options.user: + user_id = session.getUser(options.user)['id'] + else: + user_id = session.getLoggedInUser()['id'] + + if options.package: + package_id = session.getPackageID(options.package) + if package_id is None: + parser.error("No such package: %s" % options.package) + else: + package_id = None + + if options.tag: + try: + tag_id = session.getTagID(options.tag, strict=True) + except koji.GenericError: + parser.error("No such tag: %s" % options.tag) + else: + tag_id = None + + session.createNotification(user_id, package_id, tag_id, options.success_only) diff --git a/cli/koji_cli/commands/add_pkg.py b/cli/koji_cli/commands/add_pkg.py new file mode 100644 index 0000000..0c81b51 --- /dev/null +++ b/cli/koji_cli/commands/add_pkg.py @@ -0,0 +1,58 @@ +from __future__ import absolute_import, division + +from optparse import OptionParser + +import koji + +from koji_cli.lib import ( + activate_session, + error, + get_usage_str +) + + +def handle_add_pkg(goptions, session, args): + "[admin] Add a package to the listing for tag" + usage = "usage: %prog add-pkg [options] --owner [ ...]" + parser = OptionParser(usage=get_usage_str(usage)) + parser.add_option("--force", action='store_true', help="Override blocks if necessary") + parser.add_option("--owner", help="Specify owner") + parser.add_option("--extra-arches", help="Specify extra arches") + (options, args) = parser.parse_args(args) + if len(args) < 2: + parser.error("Please specify a tag and at least one package") + if not options.owner: + parser.error("Please specify an owner for the package(s)") + if not session.getUser(options.owner): + error("No such user: %s" % options.owner) + activate_session(session, goptions) + tag = args[0] + opts = {} + opts['force'] = options.force + opts['block'] = False + # check if list of packages exists for that tag already + dsttag = session.getTag(tag) + if dsttag is None: + error("No such tag: %s" % tag) + try: + pkglist = session.listPackages(tagID=dsttag['id'], with_owners=False) + except koji.ParameterError: + # performance option added in 1.25 + pkglist = session.listPackages(tagID=dsttag['id']) + pkglist = dict([(p['package_name'], p['package_id']) for p in pkglist]) + to_add = [] + for package in args[1:]: + package_id = pkglist.get(package, None) + if package_id is not None: + print("Package %s already exists in tag %s" % (package, tag)) + continue + to_add.append(package) + if options.extra_arches: + opts['extra_arches'] = koji.parse_arches(options.extra_arches) + + # add the packages + print("Adding %i packages to tag %s" % (len(to_add), dsttag['name'])) + session.multicall = True + for package in to_add: + session.packageListAdd(tag, package, options.owner, **opts) + session.multiCall(strict=True) diff --git a/cli/koji_cli/commands/add_tag.py b/cli/koji_cli/commands/add_tag.py new file mode 100644 index 0000000..05892ba --- /dev/null +++ b/cli/koji_cli/commands/add_tag.py @@ -0,0 +1,48 @@ +from __future__ import absolute_import, division + +from optparse import OptionParser + +import koji + +from koji_cli.lib import ( + activate_session, + arg_filter, + get_usage_str +) + + +def handle_add_tag(goptions, session, args): + "[admin] Add a new tag to the database" + usage = "usage: %prog add-tag [options] " + parser = OptionParser(usage=get_usage_str(usage)) + parser.add_option("--parent", help="Set a parent tag with priority 0") + parser.add_option("--arches", help="Specify arches") + parser.add_option("--maven-support", action="store_true", + help="Enable creation of Maven repos for this tag") + parser.add_option("--include-all", action="store_true", + help="Include all packages in this tag when generating Maven repos") + parser.add_option("-x", "--extra", action="append", default=[], metavar="key=value", + help="Set tag extra option") + (options, args) = parser.parse_args(args) + if len(args) != 1: + parser.error("Please specify a name for the tag") + activate_session(session, goptions) + if not (session.hasPerm('admin') or session.hasPerm('tag')): + parser.error("This action requires tag or admin privileges") + opts = {} + if options.parent: + opts['parent'] = options.parent + if options.arches: + opts['arches'] = koji.parse_arches(options.arches) + if options.maven_support: + opts['maven_support'] = True + if options.include_all: + opts['maven_include_all'] = True + if options.extra: + extra = {} + for xopt in options.extra: + key, value = xopt.split('=', 1) + value = arg_filter(value) + extra[key] = value + opts['extra'] = extra + session.createTag(args[0], **opts) diff --git a/cli/koji_cli/commands/add_tag_inheritance.py b/cli/koji_cli/commands/add_tag_inheritance.py new file mode 100644 index 0000000..29eef90 --- /dev/null +++ b/cli/koji_cli/commands/add_tag_inheritance.py @@ -0,0 +1,66 @@ +from __future__ import absolute_import, division + +from optparse import OptionParser + +from koji_cli.lib import ( + activate_session, + error, + get_usage_str, + warn +) + + +def handle_add_tag_inheritance(goptions, session, args): + """[admin] Add to a tag's inheritance""" + usage = "usage: %prog add-tag-inheritance [options] " + parser = OptionParser(usage=get_usage_str(usage)) + parser.add_option("--priority", help="Specify priority") + parser.add_option("--maxdepth", help="Specify max depth") + parser.add_option("--intransitive", action="store_true", help="Set intransitive") + parser.add_option("--noconfig", action="store_true", help="Set to packages only") + parser.add_option("--pkg-filter", help="Specify the package filter") + parser.add_option("--force", action="store_true", + help="Force adding a parent to a tag that already has that parent tag") + (options, args) = parser.parse_args(args) + + if len(args) != 2: + parser.error("This command takes exctly two argument: a tag name or ID and that tag's " + "new parent name or ID") + + activate_session(session, goptions) + + tag = session.getTag(args[0]) + if not tag: + parser.error("No such tag: %s" % args[0]) + + parent = session.getTag(args[1]) + if not parent: + parser.error("No such tag: %s" % args[1]) + + inheritanceData = session.getInheritanceData(tag['id']) + priority = options.priority and int(options.priority) or 0 + sameParents = [datum for datum in inheritanceData if datum['parent_id'] == parent['id']] + samePriority = [datum for datum in inheritanceData if datum['priority'] == priority] + + if sameParents and not options.force: + warn("Error: You are attempting to add %s as %s's parent even though it already is " + "%s's parent." + % (parent['name'], tag['name'], tag['name'])) + error("Please use --force if this is what you really want to do.") + if samePriority: + error("Error: There is already an active inheritance with that priority on %s, " + "please specify a different priority with --priority." % tag['name']) + + new_data = {} + new_data['parent_id'] = parent['id'] + new_data['priority'] = options.priority or 0 + if options.maxdepth and options.maxdepth.isdigit(): + new_data['maxdepth'] = int(options.maxdepth) + else: + new_data['maxdepth'] = None + new_data['intransitive'] = options.intransitive or False + new_data['noconfig'] = options.noconfig or False + new_data['pkg_filter'] = options.pkg_filter or '' + + inheritanceData.append(new_data) + session.setInheritanceData(tag['id'], inheritanceData) diff --git a/cli/koji_cli/commands/add_target.py b/cli/koji_cli/commands/add_target.py new file mode 100644 index 0000000..7df05a6 --- /dev/null +++ b/cli/koji_cli/commands/add_target.py @@ -0,0 +1,41 @@ +from __future__ import absolute_import, division + +from optparse import OptionParser + +from koji_cli.lib import ( + activate_session, + error, + get_usage_str +) + + +def handle_add_target(goptions, session, args): + "[admin] Create a new build target" + usage = "usage: %prog add-target " + parser = OptionParser(usage=get_usage_str(usage)) + (options, args) = parser.parse_args(args) + if len(args) < 2: + parser.error("Please specify a target name, a build tag, and destination tag") + elif len(args) > 3: + parser.error("Incorrect number of arguments") + name = args[0] + build_tag = args[1] + if len(args) > 2: + dest_tag = args[2] + else: + # most targets have the same name as their destination + dest_tag = name + activate_session(session, goptions) + if not (session.hasPerm('admin') or session.hasPerm('target')): + parser.error("This action requires target or admin privileges") + + chkbuildtag = session.getTag(build_tag) + chkdesttag = session.getTag(dest_tag) + if not chkbuildtag: + error("No such tag: %s" % build_tag) + if not chkbuildtag.get("arches", None): + error("Build tag has no arches: %s" % build_tag) + if not chkdesttag: + error("No such destination tag: %s" % dest_tag) + + session.createBuildTarget(name, build_tag, dest_tag) diff --git a/cli/koji_cli/commands/add_user.py b/cli/koji_cli/commands/add_user.py new file mode 100644 index 0000000..bf7c8b8 --- /dev/null +++ b/cli/koji_cli/commands/add_user.py @@ -0,0 +1,31 @@ +from __future__ import absolute_import, division + +from optparse import OptionParser + +import koji + +from koji_cli.lib import ( + activate_session, + get_usage_str +) + + +def handle_add_user(goptions, session, args): + "[admin] Add a user" + usage = "usage: %prog add-user [options]" + parser = OptionParser(usage=get_usage_str(usage)) + parser.add_option("--principal", help="The Kerberos principal for this user") + parser.add_option("--disable", help="Prohibit logins by this user", action="store_true") + (options, args) = parser.parse_args(args) + if len(args) < 1: + parser.error("You must specify the username of the user to add") + elif len(args) > 1: + parser.error("This command only accepts one argument (username)") + username = args[0] + if options.disable: + status = koji.USER_STATUS['BLOCKED'] + else: + status = koji.USER_STATUS['NORMAL'] + activate_session(session, goptions) + user_id = session.createUser(username, status=status, krb_principal=options.principal) + print("Added user %s (%i)" % (username, user_id)) diff --git a/cli/koji_cli/commands/add_volume.py b/cli/koji_cli/commands/add_volume.py new file mode 100644 index 0000000..6fe21e1 --- /dev/null +++ b/cli/koji_cli/commands/add_volume.py @@ -0,0 +1,25 @@ +from __future__ import absolute_import, division + +from optparse import OptionParser + +from koji_cli.lib import ( + activate_session, + error, + get_usage_str +) + + +def handle_add_volume(goptions, session, args): + "[admin] Add a new storage volume" + usage = "usage: %prog add-volume " + parser = OptionParser(usage=get_usage_str(usage)) + (options, args) = parser.parse_args(args) + if len(args) != 1: + parser.error("Command requires exactly one volume-name.") + name = args[0] + volinfo = session.getVolume(name) + if volinfo: + error("Volume %s already exists" % name) + activate_session(session, goptions) + volinfo = session.addVolume(name) + print("Added volume %(name)s with id %(id)i" % volinfo) diff --git a/cli/koji_cli/commands/assign_task.py b/cli/koji_cli/commands/assign_task.py new file mode 100644 index 0000000..e6b0296 --- /dev/null +++ b/cli/koji_cli/commands/assign_task.py @@ -0,0 +1,47 @@ +from __future__ import absolute_import, division + +from optparse import OptionParser + +import koji + +from koji_cli.lib import ( + activate_session, + get_usage_str +) + + +def handle_assign_task(goptions, session, args): + "[admin] Assign a task to a host" + usage = 'usage: %prog assign-task ' + parser = OptionParser(usage=get_usage_str(usage)) + parser.add_option('-f', '--force', action='store_true', default=False, + help='force to assign a non-free task') + (options, args) = parser.parse_args(args) + + if len(args) != 2: + parser.error('please specify a task id and a hostname') + else: + task_id = int(args[0]) + hostname = args[1] + + taskinfo = session.getTaskInfo(task_id, request=False) + if taskinfo is None: + raise koji.GenericError("No such task: %s" % task_id) + + hostinfo = session.getHost(hostname) + if hostinfo is None: + raise koji.GenericError("No such host: %s" % hostname) + + force = False + if options.force: + force = True + + activate_session(session, goptions) + if not session.hasPerm('admin'): + parser.error("This action requires admin privileges") + + ret = session.assignTask(task_id, hostname, force) + if ret: + print('assigned task %d to host %s' % (task_id, hostname)) + else: + print('failed to assign task %d to host %s' % (task_id, hostname)) diff --git a/cli/koji_cli/commands/block_group.py b/cli/koji_cli/commands/block_group.py new file mode 100644 index 0000000..82ae502 --- /dev/null +++ b/cli/koji_cli/commands/block_group.py @@ -0,0 +1,35 @@ +from __future__ import absolute_import, division + +from optparse import OptionParser + +from koji_cli.lib import ( + activate_session, + error, + get_usage_str +) + + +def handle_block_group(goptions, session, args): + "[admin] Block group in tag" + usage = "usage: %prog block-group " + parser = OptionParser(usage=get_usage_str(usage)) + (options, args) = parser.parse_args(args) + if len(args) != 2: + parser.error("Please specify a tag name and a group name") + tag = args[0] + group = args[1] + + activate_session(session, goptions) + if not (session.hasPerm('admin') or session.hasPerm('tag')): + parser.error("This action requires tag or admin privileges") + + dsttag = session.getTag(tag) + if not dsttag: + error("No such tag: %s" % tag) + + groups = dict([(p['name'], p['group_id']) for p in session.getTagGroups(tag, inherit=False)]) + group_id = groups.get(group, None) + if group_id is None: + error("Group %s doesn't exist within tag %s" % (group, tag)) + + session.groupListBlock(tag, group) diff --git a/cli/koji_cli/commands/block_group_pkg.py b/cli/koji_cli/commands/block_group_pkg.py new file mode 100644 index 0000000..727de7a --- /dev/null +++ b/cli/koji_cli/commands/block_group_pkg.py @@ -0,0 +1,24 @@ +from __future__ import absolute_import, division + +from optparse import OptionParser + +from koji_cli.lib import ( + activate_session, + get_usage_str +) + + +def handle_block_group_pkg(goptions, session, args): + "[admin] Block a package from a group's package listing" + usage = "usage: %prog block-group-pkg [options] [ ...]" + usage += '\n' + "Note that blocking is propagated through the inheritance chain, so " \ + "it is not exactly the same as package removal." + parser = OptionParser(usage=get_usage_str(usage)) + (options, args) = parser.parse_args(args) + if len(args) < 3: + parser.error("You must specify a tag name, group name, and one or more package names") + tag = args[0] + group = args[1] + activate_session(session, goptions) + for pkg in args[2:]: + session.groupPackageListBlock(tag, group, pkg) diff --git a/cli/koji_cli/commands/block_group_req.py b/cli/koji_cli/commands/block_group_req.py new file mode 100644 index 0000000..f1a8fe2 --- /dev/null +++ b/cli/koji_cli/commands/block_group_req.py @@ -0,0 +1,22 @@ +from __future__ import absolute_import, division + +from optparse import OptionParser + +from koji_cli.lib import ( + activate_session, + get_usage_str +) + + +def handle_block_group_req(goptions, session, args): + "[admin] Block a group's requirement listing" + usage = "usage: %prog block-group-req [options] " + parser = OptionParser(usage=get_usage_str(usage)) + (options, args) = parser.parse_args(args) + if len(args) != 3: + parser.error("You must specify a tag name and two group names") + tag = args[0] + group = args[1] + req = args[2] + activate_session(session, goptions) + session.groupReqListBlock(tag, group, req) diff --git a/cli/koji_cli/commands/block_notification.py b/cli/koji_cli/commands/block_notification.py new file mode 100644 index 0000000..0d40c27 --- /dev/null +++ b/cli/koji_cli/commands/block_notification.py @@ -0,0 +1,62 @@ +from __future__ import absolute_import, division + +from optparse import OptionParser + +import koji + +from koji_cli.lib import ( + activate_session, + get_usage_str +) + + +def handle_block_notification(goptions, session, args): + "[monitor] Block user's notifications" + usage = "usage: %prog block-notification [options]" + parser = OptionParser(usage=get_usage_str(usage)) + parser.add_option("--user", help="Block notifications for this user (admin-only)") + parser.add_option("--package", help="Block notifications for this package") + parser.add_option("--tag", help="Block notifications for this tag") + parser.add_option("--all", action="store_true", help="Block all notification for this user") + (options, args) = parser.parse_args(args) + + if len(args) != 0: + parser.error("This command takes no arguments") + + if not options.package and not options.tag and not options.all: + parser.error("One of --tag, --package or --all must be specified.") + + activate_session(session, goptions) + + if options.user and not session.hasPerm('admin'): + parser.error("--user requires admin permission") + + if options.user: + user_id = session.getUser(options.user, strict=True)['id'] + else: + logged_in_user = session.getLoggedInUser() + if logged_in_user: + user_id = logged_in_user['id'] + else: + parser.error("Please login with authentication or specify --user") + + if options.package: + package_id = session.getPackageID(options.package) + if package_id is None: + parser.error("No such package: %s" % options.package) + else: + package_id = None + + if options.tag: + try: + tag_id = session.getTagID(options.tag, strict=True) + except koji.GenericError: + parser.error("No such tag: %s" % options.tag) + else: + tag_id = None + + for block in session.getBuildNotificationBlocks(user_id): + if block['package_id'] == package_id and block['tag_id'] == tag_id: + parser.error('Notification already exists.') + + session.createNotificationBlock(user_id, package_id, tag_id) diff --git a/cli/koji_cli/commands/block_pkg.py b/cli/koji_cli/commands/block_pkg.py new file mode 100644 index 0000000..1fca7dc --- /dev/null +++ b/cli/koji_cli/commands/block_pkg.py @@ -0,0 +1,52 @@ +from __future__ import absolute_import, division + +from optparse import OptionParser + +import koji + +from koji_cli.lib import ( + activate_session, + error, + get_usage_str, + warn +) + + +def handle_block_pkg(goptions, session, args): + "[admin] Block a package in the listing for tag" + usage = "usage: %prog block-pkg [options] [ ...]" + parser = OptionParser(usage=get_usage_str(usage)) + parser.add_option("--force", action='store_true', default=False, + help="Override blocks and owner if necessary") + (options, args) = parser.parse_args(args) + if len(args) < 2: + parser.error("Please specify a tag and at least one package") + activate_session(session, goptions) + tag = args[0] + # check if list of packages exists for that tag already + dsttag = session.getTag(tag) + if dsttag is None: + error("No such tag: %s" % tag) + try: + pkglist = session.listPackages(tagID=dsttag['id'], inherited=True, with_owners=False) + except koji.ParameterError: + # performance option added in 1.25 + pkglist = session.listPackages(tagID=dsttag['id'], inherited=True) + pkglist = dict([(p['package_name'], p['package_id']) for p in pkglist]) + ret = 0 + for package in args[1:]: + package_id = pkglist.get(package, None) + if package_id is None: + warn("Package %s doesn't exist in tag %s" % (package, tag)) + ret = 1 + if ret: + error(code=ret) + session.multicall = True + for package in args[1:]: + # force is not supported on older hub, so use it only explicitly + # https://pagure.io/koji/issue/1388 + if options.force: + session.packageListBlock(tag, package, force=options.force) + else: + session.packageListBlock(tag, package) + session.multiCall(strict=True) diff --git a/cli/koji_cli/commands/build.py b/cli/koji_cli/commands/build.py new file mode 100644 index 0000000..96aa450 --- /dev/null +++ b/cli/koji_cli/commands/build.py @@ -0,0 +1,129 @@ +from __future__ import absolute_import, division + +import json +import os +import textwrap +from optparse import OptionParser + +import koji + +from koji_cli.lib import ( + _progress_callback, + _running_in_bg, + activate_session, + get_usage_str, + unique_path, + watch_tasks +) + + +def handle_build(options, session, args): + "[build] Build a package from source" + + usage = """\ + usage: %prog build [options] + + The first option is the build target, not to be confused with the destination + tag (where the build eventually lands) or build tag (where the buildroot + contents are pulled from). + + You can list all available build targets using the '%prog list-targets' command. + More detail can be found in the documentation. + https://docs.pagure.org/koji/HOWTO/#package-organization""" + + usage = textwrap.dedent(usage) + parser = OptionParser(usage=get_usage_str(usage)) + parser.add_option("--skip-tag", action="store_true", help="Do not attempt to tag package") + parser.add_option("--scratch", action="store_true", help="Perform a scratch build") + parser.add_option("--rebuild-srpm", action="store_true", dest="rebuild_srpm", + help="Force rebuilding SRPM for scratch build only") + parser.add_option("--no-rebuild-srpm", action="store_false", dest="rebuild_srpm", + help="Force not to rebuild srpm for scratch build only") + parser.add_option("--wait", action="store_true", + help="Wait on the build, even if running in the background") + parser.add_option("--nowait", action="store_false", dest="wait", help="Don't wait on build") + parser.add_option("--wait-repo", action="store_true", + help="Wait for the actual buildroot repo of given target") + parser.add_option("--wait-build", metavar="NVR", action="append", dest="wait_builds", + default=[], help="Wait for the given nvr to appear in buildroot repo") + parser.add_option("--quiet", action="store_true", + help="Do not print the task information", default=options.quiet) + parser.add_option("--arch-override", help="Override build arches") + parser.add_option("--fail-fast", action="store_true", + help="Override build_arch_can_fail settings and fail as fast as possible") + parser.add_option("--repo-id", type="int", help="Use a specific repo") + parser.add_option("--noprogress", action="store_true", + help="Do not display progress of the upload") + parser.add_option("--background", action="store_true", + help="Run the build at a lower priority") + parser.add_option("--custom-user-metadata", type="str", + help="Provide a JSON string of custom metadata to be deserialized and " + "stored under the build's extra.custom_user_metadata field") + (build_opts, args) = parser.parse_args(args) + if len(args) != 2: + parser.error("Exactly two arguments (a build target and a SCM URL or srpm file) are " + "required") + if build_opts.rebuild_srpm is not None and not build_opts.scratch: + parser.error("--no-/rebuild-srpm is only allowed for --scratch builds") + if build_opts.arch_override and not build_opts.scratch: + parser.error("--arch_override is only allowed for --scratch builds") + custom_user_metadata = {} + if build_opts.custom_user_metadata: + try: + custom_user_metadata = json.loads(build_opts.custom_user_metadata) + # Use ValueError instead of json.JSONDecodeError for Python 2 and 3 compatibility + except ValueError: + parser.error("--custom-user-metadata is not valid JSON") + + if not isinstance(custom_user_metadata, dict): + parser.error("--custom-user-metadata must be a JSON object") + + activate_session(session, options) + target = args[0] + if target.lower() == "none" and build_opts.repo_id: + target = None + build_opts.skip_tag = True + else: + build_target = session.getBuildTarget(target) + if not build_target: + parser.error("No such build target: %s" % target) + dest_tag = session.getTag(build_target['dest_tag']) + if not dest_tag: + parser.error("No such destination tag: %s" % build_target['dest_tag_name']) + if dest_tag['locked'] and not build_opts.scratch: + parser.error("Destination tag %s is locked" % dest_tag['name']) + source = args[1] + opts = {} + if build_opts.arch_override: + opts['arch_override'] = koji.parse_arches(build_opts.arch_override) + for key in ('skip_tag', 'scratch', 'repo_id', 'fail_fast', 'wait_repo', 'wait_builds', + 'rebuild_srpm'): + val = getattr(build_opts, key) + if val is not None: + opts[key] = val + opts["custom_user_metadata"] = custom_user_metadata + priority = None + if build_opts.background: + # relative to koji.PRIO_DEFAULT + priority = 5 + # try to check that source is an SRPM + if '://' not in source: + # treat source as an srpm and upload it + if not build_opts.quiet: + print("Uploading srpm: %s" % source) + serverdir = unique_path('cli-build') + if _running_in_bg() or build_opts.noprogress or build_opts.quiet: + callback = None + else: + callback = _progress_callback + session.uploadWrapper(source, serverdir, callback=callback) + print('') + source = "%s/%s" % (serverdir, os.path.basename(source)) + task_id = session.build(source, target, opts, priority=priority) + if not build_opts.quiet: + print("Created task: %d" % task_id) + print("Task info: %s/taskinfo?taskID=%s" % (options.weburl, task_id)) + if build_opts.wait or (build_opts.wait is None and not _running_in_bg()): + session.logout() + return watch_tasks(session, [task_id], quiet=build_opts.quiet, + poll_interval=options.poll_interval, topurl=options.topurl) diff --git a/cli/koji_cli/commands/buildinfo.py b/cli/koji_cli/commands/buildinfo.py new file mode 100644 index 0000000..ecf6493 --- /dev/null +++ b/cli/koji_cli/commands/buildinfo.py @@ -0,0 +1,116 @@ +from __future__ import absolute_import, division + +import os +from optparse import OptionParser + +import koji + +from koji_cli.lib import ( + ensure_connection, + error, + get_usage_str, + warn +) + + +def anon_handle_buildinfo(goptions, session, args): + "[info] Print basic information about a build" + usage = "usage: %prog buildinfo [options] [ ...]" + parser = OptionParser(usage=get_usage_str(usage)) + parser.add_option("--changelog", action="store_true", help="Show the changelog for the build") + (options, args) = parser.parse_args(args) + if len(args) < 1: + parser.error("Please specify a build") + ensure_connection(session, goptions) + error_hit = False + for build in args: + if build.isdigit(): + build = int(build) + info = session.getBuild(build) + if info is None: + warn("No such build: %s\n" % build) + error_hit = True + continue + task = None + if info['task_id']: + task = session.getTaskInfo(info['task_id'], request=True) + taglist = [] + for tag in session.listTags(build): + taglist.append(tag['name']) + info['arch'] = 'src' + info['state'] = koji.BUILD_STATES[info['state']] + print("BUILD: %(name)s-%(version)s-%(release)s [%(id)d]" % info) + print("State: %(state)s" % info) + if info['state'] == 'BUILDING': + print("Reserved by: %(cg_name)s" % info) + print("Built by: %(owner_name)s" % info) + source = info.get('source') + if source is not None: + print("Source: %s" % source) + if 'volume_name' in info: + print("Volume: %(volume_name)s" % info) + if task: + print("Task: %s %s" % (task['id'], koji.taskLabel(task))) + else: + print("Task: none") + print("Finished: %s" % koji.formatTimeLong(info['completion_ts'])) + maven_info = session.getMavenBuild(info['id']) + if maven_info: + print("Maven groupId: %s" % maven_info['group_id']) + print("Maven artifactId: %s" % maven_info['artifact_id']) + print("Maven version: %s" % maven_info['version']) + win_info = session.getWinBuild(info['id']) + if win_info: + print("Windows build platform: %s" % win_info['platform']) + print("Tags: %s" % ' '.join(taglist)) + if info.get('extra'): + print("Extra: %(extra)r" % info) + archives_seen = {} + maven_archives = session.listArchives(buildID=info['id'], type='maven') + if maven_archives: + print("Maven archives:") + for archive in maven_archives: + archives_seen.setdefault(archive['id'], 1) + print(os.path.join(koji.pathinfo.mavenbuild(info), + koji.pathinfo.mavenfile(archive))) + win_archives = session.listArchives(buildID=info['id'], type='win') + if win_archives: + print("Windows archives:") + for archive in win_archives: + archives_seen.setdefault(archive['id'], 1) + print(os.path.join(koji.pathinfo.winbuild(info), koji.pathinfo.winfile(archive))) + img_archives = session.listArchives(buildID=info['id'], type='image') + if img_archives: + print('Image archives:') + for archive in img_archives: + archives_seen.setdefault(archive['id'], 1) + print(os.path.join(koji.pathinfo.imagebuild(info), archive['filename'])) + archive_idx = {} + for archive in session.listArchives(buildID=info['id']): + if archive['id'] in archives_seen: + continue + archive_idx.setdefault(archive['btype'], []).append(archive) + for btype in archive_idx: + archives = archive_idx[btype] + print('%s Archives:' % btype.capitalize()) + for archive in archives: + print(os.path.join(koji.pathinfo.typedir(info, btype), archive['filename'])) + rpms = session.listRPMs(buildID=info['id']) + if rpms: + with session.multicall() as mc: + for rpm in rpms: + rpm['sigs'] = mc.queryRPMSigs(rpm['id']) + print("RPMs:") + for rpm in rpms: + line = os.path.join(koji.pathinfo.build(info), koji.pathinfo.rpm(rpm)) + keys = ', '.join(sorted([x['sigkey'] for x in rpm['sigs'].result if x['sigkey']])) + if keys: + line += '\tSignatures: %s' % keys + print(line) + if options.changelog: + changelog = session.getChangelogEntries(info['id']) + if changelog: + print("Changelog:") + print(koji.util.formatChangelog(changelog)) + if error_hit: + error() diff --git a/cli/koji_cli/commands/call.py b/cli/koji_cli/commands/call.py new file mode 100644 index 0000000..0e551f6 --- /dev/null +++ b/cli/koji_cli/commands/call.py @@ -0,0 +1,57 @@ +from __future__ import absolute_import, division + +import ast +import json +import pprint +import textwrap +from optparse import OptionParser + +from koji_cli.lib import ( + DatetimeJSONEncoder, + activate_session, + arg_filter, + get_usage_str +) + + +def handle_call(goptions, session, args): + "Execute an arbitrary XML-RPC call" + usage = """\ + usage: %prog call [options] [ ...] + + Note, that you can use global option --noauth for anonymous calls here""" + usage = textwrap.dedent(usage) + parser = OptionParser(usage=get_usage_str(usage)) + parser.add_option("--python", action="store_true", help="Use python syntax for values") + parser.add_option("--kwargs", + help="Specify keyword arguments as a dictionary (implies --python)") + parser.add_option("--json-output", action="store_true", help="Use JSON syntax for output") + (options, args) = parser.parse_args(args) + if len(args) < 1: + parser.error("Please specify the name of the XML-RPC method") + if options.kwargs: + options.python = True + if options.python and ast is None: + parser.error("The ast module is required to read python syntax") + if options.json_output and json is None: + parser.error("The json module is required to output JSON syntax") + activate_session(session, goptions) + name = args[0] + non_kw = [] + kw = {} + if options.python: + non_kw = [ast.literal_eval(a) for a in args[1:]] + if options.kwargs: + kw = ast.literal_eval(options.kwargs) + else: + for arg in args[1:]: + if arg.find('=') != -1: + key, value = arg.split('=', 1) + kw[key] = arg_filter(value) + else: + non_kw.append(arg_filter(arg)) + response = getattr(session, name).__call__(*non_kw, **kw) + if options.json_output: + print(json.dumps(response, indent=2, separators=(',', ': '), cls=DatetimeJSONEncoder)) + else: + pprint.pprint(response) diff --git a/cli/koji_cli/commands/cancel.py b/cli/koji_cli/commands/cancel.py new file mode 100644 index 0000000..b35fddd --- /dev/null +++ b/cli/koji_cli/commands/cancel.py @@ -0,0 +1,59 @@ +from __future__ import absolute_import, division + +from optparse import OptionParser + +import koji + +from koji_cli.lib import ( + activate_session, + get_usage_str, + warn +) + + +def handle_cancel(goptions, session, args): + "[build] Cancel tasks and/or builds" + usage = "usage: %prog cancel [options] [ ...]" + parser = OptionParser(usage=get_usage_str(usage)) + parser.add_option("--justone", action="store_true", help="Do not cancel subtasks") + parser.add_option("--full", action="store_true", help="Full cancellation (admin only)") + parser.add_option("--force", action="store_true", help="Allow subtasks with --full") + (options, args) = parser.parse_args(args) + if len(args) == 0: + parser.error("You must specify at least one task id or build") + activate_session(session, goptions) + tlist = [] + blist = [] + for arg in args: + try: + tlist.append(int(arg)) + except ValueError: + try: + koji.parse_NVR(arg) + blist.append(arg) + except koji.GenericError: + parser.error("please specify only task ids (integer) or builds (n-v-r)") + + results = [] + with session.multicall(strict=False, batch=100) as m: + if tlist: + opts = {} + remote_fn = m.cancelTask + if options.justone: + opts['recurse'] = False + elif options.full: + remote_fn = m.cancelTaskFull + if options.force: + opts['strict'] = False + for task_id in tlist: + results.append(remote_fn(task_id, **opts)) + for build in blist: + results.append(m.cancelBuild(build)) + + err = False + for r in results: + if isinstance(r.result, dict): + warn(r.result['faultString']) + err = True + if err: + return 1 diff --git a/cli/koji_cli/commands/chain_build.py b/cli/koji_cli/commands/chain_build.py new file mode 100644 index 0000000..29c4208 --- /dev/null +++ b/cli/koji_cli/commands/chain_build.py @@ -0,0 +1,87 @@ +from __future__ import absolute_import, division + +from optparse import OptionParser + + +from koji_cli.lib import ( + _running_in_bg, + activate_session, + error, + get_usage_str, + warn, + watch_tasks +) + + +def handle_chain_build(options, session, args): + # XXX - replace handle_build with this, once chain-building has gotten testing + "[build] Build one or more packages from source" + usage = "usage: %prog chain-build [options] [ [:] [:] ...]" + parser = OptionParser(usage=get_usage_str(usage)) + parser.add_option("--wait", action="store_true", + help="Wait on build, even if running in the background") + parser.add_option("--nowait", action="store_false", dest="wait", help="Don't wait on build") + parser.add_option("--quiet", action="store_true", + help="Do not print the task information", default=options.quiet) + parser.add_option("--background", action="store_true", + help="Run the build at a lower priority") + (build_opts, args) = parser.parse_args(args) + if len(args) < 2: + parser.error("At least two arguments (a build target and a SCM URL) are required") + activate_session(session, options) + target = args[0] + build_target = session.getBuildTarget(target) + if not build_target: + parser.error("No such build target: %s" % target) + dest_tag = session.getTag(build_target['dest_tag'], strict=True) + if dest_tag['locked']: + parser.error("Destination tag %s is locked" % dest_tag['name']) + + # check that the destination tag is in the inheritance tree of the build tag + # otherwise there is no way that a chain-build can work + ancestors = session.getFullInheritance(build_target['build_tag']) + if dest_tag['id'] not in [build_target['build_tag']] + \ + [ancestor['parent_id'] for ancestor in ancestors]: + warn("Packages in destination tag %(dest_tag_name)s are not inherited by build tag " + "%(build_tag_name)s" % build_target) + error("Target %s is not usable for a chain-build" % build_target['name']) + sources = args[1:] + + src_list = [] + build_level = [] + # src_lists is a list of lists of sources to build. + # each list is block of builds ("build level") which must all be completed + # before the next block begins. Blocks are separated on the command line with ':' + for src in sources: + if src == ':': + if build_level: + src_list.append(build_level) + build_level = [] + elif '://' in src: + # quick check that src might be a url + build_level.append(src) + elif '/' not in src and not src.endswith('.rpm') and len(src.split('-')) >= 3: + # quick check that it looks like a N-V-R + build_level.append(src) + else: + error('"%s" is not a SCM URL or package N-V-R' % src) + if build_level: + src_list.append(build_level) + + if len(src_list) < 2: + parser.error('You must specify at least one dependency between builds with : (colon)\n' + 'If there are no dependencies, use the build command instead') + + priority = None + if build_opts.background: + # relative to koji.PRIO_DEFAULT + priority = 5 + + task_id = session.chainBuild(src_list, target, priority=priority) + if not build_opts.quiet: + print("Created task: %d" % task_id) + print("Task info: %s/taskinfo?taskID=%s" % (options.weburl, task_id)) + if build_opts.wait or (build_opts.wait is None and not _running_in_bg()): + session.logout() + return watch_tasks(session, [task_id], quiet=build_opts.quiet, + poll_interval=options.poll_interval, topurl=options.topurl) diff --git a/cli/koji_cli/commands/clone_tag.py b/cli/koji_cli/commands/clone_tag.py new file mode 100644 index 0000000..7c64e1b --- /dev/null +++ b/cli/koji_cli/commands/clone_tag.py @@ -0,0 +1,543 @@ +from __future__ import absolute_import, division + +import sys +import time +from collections import OrderedDict, defaultdict +from optparse import OptionParser + +import six +from six.moves import zip + +import koji + +from koji_cli.lib import activate_session, error, get_usage_str, warn + + +def _multicall_with_check(session, batch_size): + """Helper for running multicall inside handle_clone_tag""" + err = False + for r in session.multiCall(batch=batch_size): + if isinstance(r, dict): + warn(r['faultString']) + err = True + if err: + error('Errors during the last call. Target tag could be inconsistent.') + + +def handle_clone_tag(goptions, session, args): + "[admin] Duplicate the contents of one tag onto another tag" + usage = "usage: %prog clone-tag [options] " + usage += "\nclone-tag will create the destination tag if it does not already exist" + parser = OptionParser(usage=get_usage_str(usage)) + parser.add_option('--config', action='store_true', + help="Copy config from the source to the dest tag") + parser.add_option('--groups', action='store_true', help="Copy group information") + parser.add_option('--pkgs', action='store_true', + help="Copy package list from the source to the dest tag") + parser.add_option('--builds', action='store_true', help="Tag builds into the dest tag") + parser.add_option('--all', action='store_true', + help="The same as --config --groups --pkgs --builds") + parser.add_option('--latest-only', action='store_true', + help="Tag only the latest build of each package") + parser.add_option('--inherit-builds', action='store_true', + help="Include all builds inherited into the source tag into the dest tag") + parser.add_option('--ts', type='int', metavar="TIMESTAMP", + help='Clone tag at last event before specific timestamp') + parser.add_option('--no-delete', action='store_false', dest="delete", default=True, + help="Don't delete any existing content in dest tag.") + parser.add_option('--event', type='int', help='Clone tag at a specific event') + parser.add_option('--repo', type='int', help='Clone tag at a specific repo event') + parser.add_option("-v", "--verbose", action="store_true", help="show changes") + parser.add_option("--notify", action="store_true", default=False, + help='Send tagging/untagging notifications') + parser.add_option("-f", "--force", action="store_true", + help="override tag locks if necessary") + parser.add_option("-n", "--test", action="store_true", help="test mode") + parser.add_option("--batch", type='int', default=100, metavar='SIZE', + help="batch size of multicalls [0 to disable, default: %default]") + (options, args) = parser.parse_args(args) + + if len(args) != 2: + parser.error("This command takes two arguments: ") + activate_session(session, goptions) + + if not options.test and not (session.hasPerm('admin') or session.hasPerm('tag')): + parser.error("This action requires tag or admin privileges") + + if args[0] == args[1]: + parser.error('Source and destination tags must be different.') + + if options.batch < 0: + parser.error("batch size must be bigger than zero") + + if options.all: + options.config = options.groups = options.pkgs = options.builds = True + + event = koji.util.eventFromOpts(session, options) or {} + if event: + event['timestr'] = time.asctime(time.localtime(event['ts'])) + print("Cloning at event %(id)i (%(timestr)s)" % event) + + # store tags. + try: + srctag = session.getBuildConfig(args[0], event=event.get('id')) + except koji.GenericError: + parser.error("No such src-tag: %s" % args[0]) + dsttag = session.getTag(args[1]) + if not srctag: + parser.error("No such src-tag: %s" % args[0]) + if (srctag['locked'] and not options.force) \ + or (dsttag and dsttag['locked'] and not options.force): + parser.error("Error: You are attempting to clone from or to a tag which is locked.\n" + "Please use --force if this is what you really want to do.") + + # init debug lists. + chgpkglist = [] + chgbldlist = [] + chggrplist = [] + # case of brand new dst-tag. + if not dsttag: + # create a new tag, copy srctag header. + if not options.test: + if options.config: + session.createTag(args[1], parent=None, arches=srctag['arches'], + perm=srctag['perm_id'], + locked=srctag['locked'], + maven_support=srctag['maven_support'], + maven_include_all=srctag['maven_include_all'], + extra=srctag['extra']) + else: + session.createTag(args[1], parent=None) + # store the new tag, need its assigned id. + newtag = session.getTag(args[1], strict=True) + # get pkglist of src-tag, including inherited packages. + if options.pkgs: + srcpkgs = session.listPackages(tagID=srctag['id'], + inherited=True, + event=event.get('id')) + srcpkgs.sort(key=lambda x: x['package_name']) + if not options.test: + session.multicall = True + for pkgs in srcpkgs: + # for each package add one entry in the new tag. + chgpkglist.append(('[new]', + pkgs['package_name'], + pkgs['blocked'], + pkgs['owner_name'], + pkgs['tag_name'])) + if not options.test: + # add packages. + session.packageListAdd(newtag['name'], + pkgs['package_name'], + owner=pkgs['owner_name'], + block=pkgs['blocked'], + extra_arches=pkgs['extra_arches']) + if not options.test: + _multicall_with_check(session, options.batch) + if options.builds: + # get --all latest builds from src tag + builds = reversed(session.listTagged(srctag['id'], + event=event.get('id'), + inherit=options.inherit_builds, + latest=options.latest_only)) + if not options.test: + session.multicall = True + for build in builds: + # add missing 'name' field. + build['name'] = build['package_name'] + chgbldlist.append(('[new]', + build['package_name'], + build['nvr'], + koji.BUILD_STATES[build['state']], + build['owner_name'], + build['tag_name'])) + # copy latest builds into new tag + if not options.test: + session.tagBuildBypass(newtag['name'], + build, + force=options.force, + notify=options.notify) + if not options.test: + _multicall_with_check(session, options.batch) + if options.groups: + # Copy the group data + srcgroups = session.getTagGroups(srctag['name'], + event=event.get('id')) + if not options.test: + session.multicall = True + for group in srcgroups: + if not options.test: + session.groupListAdd(newtag['name'], group['name']) + for pkg in group['packagelist']: + if not options.test: + session.groupPackageListAdd(newtag['name'], + group['name'], + pkg['package'], + block=pkg['blocked']) + chggrplist.append(('[new]', pkg['package'], group['name'])) + if not options.test: + _multicall_with_check(session, options.batch) + # case of existing dst-tag. + if dsttag: + if options.config and not options.test: + if dsttag['extra']: + session.editTag2(dsttag['id'], remove_extra=list(dsttag['extra'].keys())) + session.editTag2(dsttag['id'], parent=None, arches=srctag['arches'], + perm=srctag['perm_id'], + locked=srctag['locked'], + maven_support=srctag['maven_support'], + maven_include_all=srctag['maven_include_all'], + extra=srctag['extra']) + dsttag = session.getTag(dsttag['id'], strict=True) + + # get fresh list of packages & builds into maps. + srcpkgs = {} + dstpkgs = {} + srcbldsbypkg = defaultdict(OrderedDict) + dstbldsbypkg = defaultdict(OrderedDict) + srcgroups = OrderedDict() + dstgroups = OrderedDict() + # we use OrderedDict so that these indexes preserve the order given to us + if options.pkgs: + for pkg in session.listPackages(tagID=srctag['id'], + inherited=True, + event=event.get('id')): + srcpkgs[pkg['package_name']] = pkg + for pkg in session.listPackages(tagID=dsttag['id'], + inherited=True): + dstpkgs[pkg['package_name']] = pkg + if options.builds: + # listTagged orders builds latest-first + # so reversing that gives us oldest-first + for build in reversed(session.listTagged(srctag['id'], + event=event.get('id'), + inherit=options.inherit_builds, + latest=options.latest_only)): + srcbldsbypkg[build['package_name']][build['nvr']] = build + # get builds in dsttag without inheritance. + # latest=False to get all builds even when latest_only = True, + # so that only the *latest* build per tag will live in. + for build in reversed(session.listTagged(dsttag['id'], + inherit=False, + latest=False)): + dstbldsbypkg[build['package_name']][build['nvr']] = build + if options.groups: + for group in session.getTagGroups(srctag['name'], + event=event.get('id')): + srcgroups[group['name']] = group + for group in session.getTagGroups(dsttag['name']): + dstgroups[group['name']] = group + # construct to-do lists. + paddlist = [] # list containing new packages to be added from src tag + for (package_name, pkg) in six.iteritems(srcpkgs): + if package_name not in dstpkgs: + paddlist.append(pkg) + paddlist.sort(key=lambda x: x['package_name']) + pdellist = [] # list containing packages no more present in dst tag + for (package_name, pkg) in six.iteritems(dstpkgs): + if package_name not in srcpkgs: + pdellist.append(pkg) + pdellist.sort(key=lambda x: x['package_name']) + baddlist = [] # list containing new builds to be added from src tag + bdellist = [] # list containing new builds to be removed from dst tag + if options.delete: + # remove builds for packages that are absent from src tag + for (pkg, dstblds) in six.iteritems(dstbldsbypkg): + if pkg not in srcbldsbypkg: + bdellist.extend(dstblds.values()) + # add and/or remove builds from dst to match src contents and order + for (pkg, srcblds) in six.iteritems(srcbldsbypkg): + dstblds = dstbldsbypkg[pkg] + ablds = [] + dblds = [] + # firstly, deal with extra builds in dst + removed_nvrs = set(dstblds.keys()) - set(srcblds.keys()) + bld_order = srcblds.copy() + if options.delete: + # mark the extra builds for deletion + dnvrs = [] + for (dstnvr, dstbld) in six.iteritems(dstblds): + if dstnvr in removed_nvrs: + dnvrs.append(dstnvr) + dblds.append(dstbld) + # we also remove them from dstblds now so that they do not + # interfere with the order comparison below + for dnvr in dnvrs: + del dstblds[dnvr] + else: + # in the no-delete case, the extra builds should be forced + # to last in the tag + bld_order = OrderedDict() + for (dstnvr, dstbld) in six.iteritems(dstblds): + if dstnvr in removed_nvrs: + bld_order[dstnvr] = dstbld + for (nvr, srcbld) in six.iteritems(srcblds): + bld_order[nvr] = srcbld + # secondly, add builds from src tag and adjust the order + for (nvr, srcbld) in six.iteritems(bld_order): + found = False + out_of_order = [] + # note that dstblds is trimmed as we go, so we are only + # considering the tail corresponding to where we are at + # in the srcblds loop + for (dstnvr, dstbld) in six.iteritems(dstblds): + if nvr == dstnvr: + found = True + break + else: + out_of_order.append(dstnvr) + dblds.append(dstbld) + for dnvr in out_of_order: + del dstblds[dnvr] + # these will be re-added in the proper order later + if found: + # remove it for next pass so we stay aligned with outer + # loop + del dstblds[nvr] + else: + # missing from dst, so we need to add it + ablds.append(srcbld) + baddlist.extend(ablds) + bdellist.extend(dblds) + baddlist.sort(key=lambda x: x['package_name']) + bdellist.sort(key=lambda x: x['package_name']) + + gaddlist = [] # list containing new groups to be added from src tag + for (grpname, group) in six.iteritems(srcgroups): + if grpname not in dstgroups: + gaddlist.append(group) + gdellist = [] # list containing groups to be removed from src tag + for (grpname, group) in six.iteritems(dstgroups): + if grpname not in srcgroups: + gdellist.append(group) + grpchanges = OrderedDict() # dict of changes to make in shared groups + for (grpname, group) in six.iteritems(srcgroups): + if grpname in dstgroups: + dstgroup = dstgroups[grpname] + grpchanges[grpname] = {'adds': [], 'dels': []} + # Store whether group is inherited or not + grpchanges[grpname]['inherited'] = False + if dstgroup['tag_id'] != dsttag['id']: + grpchanges[grpname]['inherited'] = True + srcgrppkglist = [] + dstgrppkglist = [] + for pkg in group['packagelist']: + srcgrppkglist.append(pkg['package']) + for pkg in dstgroups[grpname]['packagelist']: + dstgrppkglist.append(pkg['package']) + for pkg in srcgrppkglist: + if pkg not in dstgrppkglist: + grpchanges[grpname]['adds'].append(pkg) + for pkg in dstgrppkglist: + if pkg not in srcgrppkglist: + grpchanges[grpname]['dels'].append(pkg) + # ADD new packages. + if not options.test: + session.multicall = True + for pkg in paddlist: + chgpkglist.append(('[add]', + pkg['package_name'], + pkg['blocked'], + pkg['owner_name'], + pkg['tag_name'])) + if not options.test: + session.packageListAdd(dsttag['name'], + pkg['package_name'], + owner=pkg['owner_name'], + block=pkg['blocked'], + extra_arches=pkg['extra_arches']) + if not options.test: + _multicall_with_check(session, options.batch) + # DEL builds. To keep the order we should untag builds at first + if not options.test: + session.multicall = True + for build in bdellist: + # don't delete an inherited build. + if build['tag_name'] == dsttag['name']: + # add missing 'name' field + build['name'] = build['package_name'] + chgbldlist.append(('[del]', + build['package_name'], + build['nvr'], + koji.BUILD_STATES[build['state']], + build['owner_name'], + build['tag_name'])) + # go on del builds from new tag. + if not options.test: + session.untagBuildBypass(dsttag['name'], + build, + force=options.force, + notify=options.notify) + if not options.test: + _multicall_with_check(session, options.batch) + # ADD builds. + if not options.test: + session.multicall = True + for build in baddlist: + # add missing 'name' field. + build['name'] = build['package_name'] + chgbldlist.append(('[add]', + build['package_name'], + build['nvr'], + koji.BUILD_STATES[build['state']], + build['owner_name'], + build['tag_name'])) + # copy latest builds into new tag. + if not options.test: + session.tagBuildBypass(dsttag['name'], + build, + force=options.force, + notify=options.notify) + if not options.test: + _multicall_with_check(session, options.batch) + # ADD groups. + if not options.test: + session.multicall = True + for group in gaddlist: + if not options.test: + session.groupListAdd(dsttag['name'], + group['name'], + force=options.force) + for pkg in group['packagelist']: + if not options.test: + session.groupPackageListAdd(dsttag['name'], + group['name'], + pkg['package'], + force=options.force) + chggrplist.append(('[new]', pkg['package'], group['name'])) + if not options.test: + _multicall_with_check(session, options.batch) + # ADD group pkgs. + if not options.test: + session.multicall = True + for group in grpchanges: + for pkg in grpchanges[group]['adds']: + chggrplist.append(('[new]', pkg, group)) + if not options.test: + session.groupPackageListAdd(dsttag['name'], + group, + pkg, + force=options.force) + if not options.test: + _multicall_with_check(session, options.batch) + if options.delete: + # DEL packages + ninhrtpdellist = [] + inhrtpdellist = [] + for pkg in pdellist: + if pkg['tag_name'] == dsttag['name']: + ninhrtpdellist.append(pkg) + else: + inhrtpdellist.append(pkg) + session.multicall = True + # delete only non-inherited packages. + for pkg in ninhrtpdellist: + # check if package have owned builds inside. + session.listTagged(dsttag['name'], + package=pkg['package_name'], + inherit=False) + bump_builds = session.multiCall(batch=options.batch) + if not options.test: + session.multicall = True + for pkg, [builds] in zip(ninhrtpdellist, bump_builds): + if isinstance(builds, dict): + error(builds['faultString']) + # remove all its builds first if there are any. + for build in builds: + # add missing 'name' field. + build['name'] = build['package_name'] + chgbldlist.append(('[del]', + build['package_name'], + build['nvr'], + koji.BUILD_STATES[build['state']], + build['owner_name'], + build['tag_name'])) + # so delete latest build(s) from new tag. + if not options.test: + session.untagBuildBypass(dsttag['name'], + build, + force=options.force, + notify=options.notify) + # now safe to remove package itself since we resolved its builds. + chgpkglist.append(('[del]', + pkg['package_name'], + pkg['blocked'], + pkg['owner_name'], + pkg['tag_name'])) + if not options.test: + session.packageListRemove(dsttag['name'], + pkg['package_name'], + force=False) + # mark as blocked inherited packages. + for pkg in inhrtpdellist: + chgpkglist.append(('[blk]', + pkg['package_name'], + pkg['blocked'], + pkg['owner_name'], + pkg['tag_name'])) + if not options.test: + session.packageListBlock(dsttag['name'], pkg['package_name']) + if not options.test: + _multicall_with_check(session, options.batch) + # DEL groups. + if not options.test: + session.multicall = True + for group in gdellist: + # Only delete a group that isn't inherited + if group['tag_id'] == dsttag['id']: + if not options.test: + session.groupListRemove(dsttag['name'], + group['name'], + force=options.force) + for pkg in group['packagelist']: + chggrplist.append(('[del]', pkg['package'], group['name'])) + # mark as blocked inherited groups. + else: + if not options.test: + session.groupListBlock(dsttag['name'], group['name']) + for pkg in group['packagelist']: + chggrplist.append(('[blk]', pkg['package'], group['name'])) + if not options.test: + _multicall_with_check(session, options.batch) + # DEL group pkgs. + if not options.test: + session.multicall = True + for group in grpchanges: + for pkg in grpchanges[group]['dels']: + # Only delete a group that isn't inherited + if not grpchanges[group]['inherited']: + chggrplist.append(('[del]', pkg, group)) + if not options.test: + session.groupPackageListRemove(dsttag['name'], + group, + pkg, + force=options.force) + else: + chggrplist.append(('[blk]', pkg, group)) + if not options.test: + session.groupPackageListBlock(dsttag['name'], + group, + pkg) + if not options.test: + _multicall_with_check(session, options.batch) + # print final list of actions. + if options.verbose: + pfmt = ' %-7s %-28s %-10s %-10s %-10s\n' + bfmt = ' %-7s %-28s %-40s %-10s %-10s %-10s\n' + gfmt = ' %-7s %-28s %-28s\n' + sys.stdout.write('\nList of changes:\n\n') + sys.stdout.write(pfmt % ('Action', 'Package', 'Blocked', 'Owner', 'From Tag')) + sys.stdout.write(pfmt % ('-' * 7, '-' * 28, '-' * 10, '-' * 10, '-' * 10)) + for changes in chgpkglist: + sys.stdout.write(pfmt % changes) + sys.stdout.write('\n') + sys.stdout.write(bfmt % + ('Action', 'From/To Package', 'Build(s)', 'State', 'Owner', 'From Tag')) + sys.stdout.write(bfmt % ('-' * 7, '-' * 28, '-' * 40, '-' * 10, '-' * 10, '-' * 10)) + for changes in chgbldlist: + sys.stdout.write(bfmt % changes) + sys.stdout.write('\n') + sys.stdout.write(gfmt % ('Action', 'Package', 'Group')) + sys.stdout.write(gfmt % ('-' * 7, '-' * 28, '-' * 28)) + for changes in chggrplist: + sys.stdout.write(gfmt % changes) diff --git a/cli/koji_cli/commands/cmd_add_channel.py b/cli/koji_cli/commands/cmd_add_channel.py deleted file mode 100644 index a954c8d..0000000 --- a/cli/koji_cli/commands/cmd_add_channel.py +++ /dev/null @@ -1,39 +0,0 @@ -from __future__ import absolute_import, division - -from optparse import OptionParser - - -import koji - -from koji_cli.lib import ( - activate_session, - error, - get_usage_str -) - - -def handle_add_channel(goptions, session, args): - "[admin] Add a channel" - usage = "usage: %prog add-channel [options] " - parser = OptionParser(usage=get_usage_str(usage)) - parser.add_option("--description", help="Description of channel") - (options, args) = parser.parse_args(args) - if len(args) != 1: - parser.error("Please specify one channel name") - activate_session(session, goptions) - channel_name = args[0] - try: - channel_id = session.addChannel(channel_name, description=options.description) - except koji.GenericError as ex: - msg = str(ex) - if 'channel %s already exists' % channel_name in msg: - error("channel %s already exists" % channel_name) - elif 'Invalid method:' in msg: - version = session.getKojiVersion() - error("addChannel is available on hub from Koji 1.26 version, your version is %s" % - version) - else: - error(msg) - print("%s added: id %d" % (args[0], channel_id)) - - diff --git a/cli/koji_cli/commands/cmd_add_external_repo.py b/cli/koji_cli/commands/cmd_add_external_repo.py deleted file mode 100644 index 78d7c7e..0000000 --- a/cli/koji_cli/commands/cmd_add_external_repo.py +++ /dev/null @@ -1,60 +0,0 @@ -from __future__ import absolute_import, division - -from optparse import OptionParser - - -import koji - -from koji_cli.lib import ( - activate_session, - get_usage_str -) - - -def handle_add_external_repo(goptions, session, args): - "[admin] Create an external repo and/or add one to a tag" - usage = "usage: %prog add-external-repo [options] []" - parser = OptionParser(usage=get_usage_str(usage)) - parser.add_option("-t", "--tag", action="append", metavar="TAG", - help="Also add repo to tag. Use tag::N to set priority") - parser.add_option("-p", "--priority", type='int', - help="Set priority (when adding to tag)") - parser.add_option("-m", "--mode", help="Set merge mode") - parser.add_option("-a", "--arches", metavar="ARCH1,ARCH2, ...", - help="Use only subset of arches from given repo") - (options, args) = parser.parse_args(args) - activate_session(session, goptions) - if options.mode: - if options.mode not in koji.REPO_MERGE_MODES: - parser.error('Invalid mode: %s' % options.mode) - if not options.tag: - parser.error('The --mode option can only be used with --tag') - if len(args) == 1: - name = args[0] - rinfo = session.getExternalRepo(name, strict=True) - if not options.tag: - parser.error("A url is required to create an external repo entry") - elif len(args) == 2: - name, url = args - rinfo = session.createExternalRepo(name, url) - print("Created external repo %(id)i" % rinfo) - else: - parser.error("Incorrect number of arguments") - if options.tag: - for tagpri in options.tag: - tag, priority = _parse_tagpri(tagpri) - if priority is None: - if options.priority is not None: - priority = options.priority - else: - priority = _pick_external_repo_priority(session, tag) - callopts = {} - if options.mode: - callopts['merge_mode'] = options.mode - if options.arches: - callopts['arches'] = options.arches - session.addExternalRepoToTag(tag, rinfo['name'], priority, **callopts) - print("Added external repo %s to tag %s (priority %i)" - % (rinfo['name'], tag, priority)) - - diff --git a/cli/koji_cli/commands/cmd_add_group.py b/cli/koji_cli/commands/cmd_add_group.py deleted file mode 100644 index d0a28c3..0000000 --- a/cli/koji_cli/commands/cmd_add_group.py +++ /dev/null @@ -1,39 +0,0 @@ -from __future__ import absolute_import, division - -from optparse import OptionParser - - - -from koji_cli.lib import ( - activate_session, - error, - get_usage_str -) - - -def handle_add_group(goptions, session, args): - "[admin] Add a group to a tag" - usage = "usage: %prog add-group " - parser = OptionParser(usage=get_usage_str(usage)) - (options, args) = parser.parse_args(args) - if len(args) != 2: - parser.error("Please specify a tag name and a group name") - tag = args[0] - group = args[1] - - activate_session(session, goptions) - if not (session.hasPerm('admin') or session.hasPerm('tag')): - parser.error("This action requires tag or admin privileges") - - dsttag = session.getTag(tag) - if not dsttag: - error("No such tag: %s" % tag) - - groups = dict([(p['name'], p['group_id']) for p in session.getTagGroups(tag, inherit=False)]) - group_id = groups.get(group, None) - if group_id is not None: - error("Group %s already exists for tag %s" % (group, tag)) - - session.groupListAdd(tag, group) - - diff --git a/cli/koji_cli/commands/cmd_add_group_pkg.py b/cli/koji_cli/commands/cmd_add_group_pkg.py deleted file mode 100644 index 0bf0b29..0000000 --- a/cli/koji_cli/commands/cmd_add_group_pkg.py +++ /dev/null @@ -1,26 +0,0 @@ -from __future__ import absolute_import, division - -from optparse import OptionParser - - - -from koji_cli.lib import ( - activate_session, - get_usage_str -) - - -def handle_add_group_pkg(goptions, session, args): - "[admin] Add a package to a group's package listing" - usage = "usage: %prog add-group-pkg [options] [ ...]" - parser = OptionParser(usage=get_usage_str(usage)) - (options, args) = parser.parse_args(args) - if len(args) < 3: - parser.error("You must specify a tag name, group name, and one or more package names") - tag = args[0] - group = args[1] - activate_session(session, goptions) - for pkg in args[2:]: - session.groupPackageListAdd(tag, group, pkg) - - diff --git a/cli/koji_cli/commands/cmd_add_group_req.py b/cli/koji_cli/commands/cmd_add_group_req.py deleted file mode 100644 index e1b88d6..0000000 --- a/cli/koji_cli/commands/cmd_add_group_req.py +++ /dev/null @@ -1,26 +0,0 @@ -from __future__ import absolute_import, division - -from optparse import OptionParser - - - -from koji_cli.lib import ( - activate_session, - get_usage_str -) - - -def handle_add_group_req(goptions, session, args): - "[admin] Add a group to a group's required list" - usage = "usage: %prog add-group-req [options] " - parser = OptionParser(usage=get_usage_str(usage)) - (options, args) = parser.parse_args(args) - if len(args) != 3: - parser.error("You must specify a tag name and two group names") - tag = args[0] - group = args[1] - req = args[2] - activate_session(session, goptions) - session.groupReqListAdd(tag, group, req) - - diff --git a/cli/koji_cli/commands/cmd_add_host.py b/cli/koji_cli/commands/cmd_add_host.py deleted file mode 100644 index e38b904..0000000 --- a/cli/koji_cli/commands/cmd_add_host.py +++ /dev/null @@ -1,37 +0,0 @@ -from __future__ import absolute_import, division - -from optparse import OptionParser - - - -from koji_cli.lib import ( - activate_session, - error, - get_usage_str -) - - -def handle_add_host(goptions, session, args): - "[admin] Add a host" - usage = "usage: %prog add-host [options] [ ...]" - parser = OptionParser(usage=get_usage_str(usage)) - parser.add_option("--krb-principal", - help="set a non-default kerberos principal for the host") - parser.add_option("--force", default=False, action="store_true", - help="if existing used is a regular user, convert it to a host") - (options, args) = parser.parse_args(args) - if len(args) < 2: - parser.error("Please specify a hostname and at least one arch") - host = args[0] - activate_session(session, goptions) - id = session.getHost(host) - if id: - error("%s is already in the database" % host) - else: - kwargs = {'force': options.force} - if options.krb_principal is not None: - kwargs['krb_principal'] = options.krb_principal - id = session.addHost(host, args[1:], **kwargs) - print("%s added: id %d" % (host, id)) - - diff --git a/cli/koji_cli/commands/cmd_add_host_to_channel.py b/cli/koji_cli/commands/cmd_add_host_to_channel.py deleted file mode 100644 index ae375f5..0000000 --- a/cli/koji_cli/commands/cmd_add_host_to_channel.py +++ /dev/null @@ -1,46 +0,0 @@ -from __future__ import absolute_import, division - -from optparse import SUPPRESS_HELP, OptionParser - - - -from koji_cli.lib import ( - activate_session, - error, - get_usage_str -) - - -def handle_add_host_to_channel(goptions, session, args): - "[admin] Add a host to a channel" - usage = "usage: %prog add-host-to-channel [options] " - parser = OptionParser(usage=get_usage_str(usage)) - parser.add_option("--list", action="store_true", help=SUPPRESS_HELP) - parser.add_option("--new", action="store_true", help="Create channel if needed") - parser.add_option("--force", action="store_true", help="force added, if possible") - (options, args) = parser.parse_args(args) - if not options.list and len(args) != 2: - parser.error("Please specify a hostname and a channel") - activate_session(session, goptions) - if options.list: - for channel in session.listChannels(): - print(channel['name']) - return - channel = args[1] - if not options.new: - channelinfo = session.getChannel(channel) - if not channelinfo: - error("No such channel: %s" % channel) - - host = args[0] - hostinfo = session.getHost(host) - if not hostinfo: - error("No such host: %s" % host) - kwargs = {} - if options.new: - kwargs['create'] = True - if options.force: - kwargs['force'] = True - session.addHostToChannel(host, channel, **kwargs) - - diff --git a/cli/koji_cli/commands/cmd_add_notification.py b/cli/koji_cli/commands/cmd_add_notification.py deleted file mode 100644 index 1c688ab..0000000 --- a/cli/koji_cli/commands/cmd_add_notification.py +++ /dev/null @@ -1,57 +0,0 @@ -from __future__ import absolute_import, division - -from optparse import OptionParser - - -import koji - -from koji_cli.lib import ( - activate_session, - get_usage_str -) - - -def handle_add_notification(goptions, session, args): - "[monitor] Add user's notification" - usage = "usage: %prog add-notification [options]" - parser = OptionParser(usage=get_usage_str(usage)) - parser.add_option("--user", help="Add notifications for this user (admin-only)") - parser.add_option("--package", help="Add notifications for this package") - parser.add_option("--tag", help="Add notifications for this tag") - parser.add_option("--success-only", action="store_true", default=False, help="") - (options, args) = parser.parse_args(args) - - if len(args) != 0: - parser.error("This command takes no arguments") - - if not options.package and not options.tag: - parser.error("Command need at least one from --tag or --package options.") - - activate_session(session, goptions) - - if options.user and not session.hasPerm('admin'): - parser.error("--user requires admin permission") - - if options.user: - user_id = session.getUser(options.user)['id'] - else: - user_id = session.getLoggedInUser()['id'] - - if options.package: - package_id = session.getPackageID(options.package) - if package_id is None: - parser.error("No such package: %s" % options.package) - else: - package_id = None - - if options.tag: - try: - tag_id = session.getTagID(options.tag, strict=True) - except koji.GenericError: - parser.error("No such tag: %s" % options.tag) - else: - tag_id = None - - session.createNotification(user_id, package_id, tag_id, options.success_only) - - diff --git a/cli/koji_cli/commands/cmd_add_pkg.py b/cli/koji_cli/commands/cmd_add_pkg.py deleted file mode 100644 index 349e281..0000000 --- a/cli/koji_cli/commands/cmd_add_pkg.py +++ /dev/null @@ -1,61 +0,0 @@ -from __future__ import absolute_import, division - -from optparse import OptionParser - - -import koji - -from koji_cli.lib import ( - activate_session, - error, - get_usage_str -) - - -def handle_add_pkg(goptions, session, args): - "[admin] Add a package to the listing for tag" - usage = "usage: %prog add-pkg [options] --owner [ ...]" - parser = OptionParser(usage=get_usage_str(usage)) - parser.add_option("--force", action='store_true', help="Override blocks if necessary") - parser.add_option("--owner", help="Specify owner") - parser.add_option("--extra-arches", help="Specify extra arches") - (options, args) = parser.parse_args(args) - if len(args) < 2: - parser.error("Please specify a tag and at least one package") - if not options.owner: - parser.error("Please specify an owner for the package(s)") - if not session.getUser(options.owner): - error("No such user: %s" % options.owner) - activate_session(session, goptions) - tag = args[0] - opts = {} - opts['force'] = options.force - opts['block'] = False - # check if list of packages exists for that tag already - dsttag = session.getTag(tag) - if dsttag is None: - error("No such tag: %s" % tag) - try: - pkglist = session.listPackages(tagID=dsttag['id'], with_owners=False) - except koji.ParameterError: - # performance option added in 1.25 - pkglist = session.listPackages(tagID=dsttag['id']) - pkglist = dict([(p['package_name'], p['package_id']) for p in pkglist]) - to_add = [] - for package in args[1:]: - package_id = pkglist.get(package, None) - if package_id is not None: - print("Package %s already exists in tag %s" % (package, tag)) - continue - to_add.append(package) - if options.extra_arches: - opts['extra_arches'] = koji.parse_arches(options.extra_arches) - - # add the packages - print("Adding %i packages to tag %s" % (len(to_add), dsttag['name'])) - session.multicall = True - for package in to_add: - session.packageListAdd(tag, package, options.owner, **opts) - session.multiCall(strict=True) - - diff --git a/cli/koji_cli/commands/cmd_add_tag.py b/cli/koji_cli/commands/cmd_add_tag.py deleted file mode 100644 index 2293d56..0000000 --- a/cli/koji_cli/commands/cmd_add_tag.py +++ /dev/null @@ -1,51 +0,0 @@ -from __future__ import absolute_import, division - -from optparse import OptionParser - - -import koji - -from koji_cli.lib import ( - activate_session, - arg_filter, - get_usage_str -) - - -def handle_add_tag(goptions, session, args): - "[admin] Add a new tag to the database" - usage = "usage: %prog add-tag [options] " - parser = OptionParser(usage=get_usage_str(usage)) - parser.add_option("--parent", help="Set a parent tag with priority 0") - parser.add_option("--arches", help="Specify arches") - parser.add_option("--maven-support", action="store_true", - help="Enable creation of Maven repos for this tag") - parser.add_option("--include-all", action="store_true", - help="Include all packages in this tag when generating Maven repos") - parser.add_option("-x", "--extra", action="append", default=[], metavar="key=value", - help="Set tag extra option") - (options, args) = parser.parse_args(args) - if len(args) != 1: - parser.error("Please specify a name for the tag") - activate_session(session, goptions) - if not (session.hasPerm('admin') or session.hasPerm('tag')): - parser.error("This action requires tag or admin privileges") - opts = {} - if options.parent: - opts['parent'] = options.parent - if options.arches: - opts['arches'] = koji.parse_arches(options.arches) - if options.maven_support: - opts['maven_support'] = True - if options.include_all: - opts['maven_include_all'] = True - if options.extra: - extra = {} - for xopt in options.extra: - key, value = xopt.split('=', 1) - value = arg_filter(value) - extra[key] = value - opts['extra'] = extra - session.createTag(args[0], **opts) - - diff --git a/cli/koji_cli/commands/cmd_add_tag_inheritance.py b/cli/koji_cli/commands/cmd_add_tag_inheritance.py deleted file mode 100644 index 98a30ae..0000000 --- a/cli/koji_cli/commands/cmd_add_tag_inheritance.py +++ /dev/null @@ -1,70 +0,0 @@ -from __future__ import absolute_import, division - -from optparse import OptionParser - - - -from koji_cli.lib import ( - activate_session, - error, - get_usage_str, - warn -) - - -def handle_add_tag_inheritance(goptions, session, args): - """[admin] Add to a tag's inheritance""" - usage = "usage: %prog add-tag-inheritance [options] " - parser = OptionParser(usage=get_usage_str(usage)) - parser.add_option("--priority", help="Specify priority") - parser.add_option("--maxdepth", help="Specify max depth") - parser.add_option("--intransitive", action="store_true", help="Set intransitive") - parser.add_option("--noconfig", action="store_true", help="Set to packages only") - parser.add_option("--pkg-filter", help="Specify the package filter") - parser.add_option("--force", action="store_true", - help="Force adding a parent to a tag that already has that parent tag") - (options, args) = parser.parse_args(args) - - if len(args) != 2: - parser.error("This command takes exctly two argument: a tag name or ID and that tag's " - "new parent name or ID") - - activate_session(session, goptions) - - tag = session.getTag(args[0]) - if not tag: - parser.error("No such tag: %s" % args[0]) - - parent = session.getTag(args[1]) - if not parent: - parser.error("No such tag: %s" % args[1]) - - inheritanceData = session.getInheritanceData(tag['id']) - priority = options.priority and int(options.priority) or 0 - sameParents = [datum for datum in inheritanceData if datum['parent_id'] == parent['id']] - samePriority = [datum for datum in inheritanceData if datum['priority'] == priority] - - if sameParents and not options.force: - warn("Error: You are attempting to add %s as %s's parent even though it already is " - "%s's parent." - % (parent['name'], tag['name'], tag['name'])) - error("Please use --force if this is what you really want to do.") - if samePriority: - error("Error: There is already an active inheritance with that priority on %s, " - "please specify a different priority with --priority." % tag['name']) - - new_data = {} - new_data['parent_id'] = parent['id'] - new_data['priority'] = options.priority or 0 - if options.maxdepth and options.maxdepth.isdigit(): - new_data['maxdepth'] = int(options.maxdepth) - else: - new_data['maxdepth'] = None - new_data['intransitive'] = options.intransitive or False - new_data['noconfig'] = options.noconfig or False - new_data['pkg_filter'] = options.pkg_filter or '' - - inheritanceData.append(new_data) - session.setInheritanceData(tag['id'], inheritanceData) - - diff --git a/cli/koji_cli/commands/cmd_add_target.py b/cli/koji_cli/commands/cmd_add_target.py deleted file mode 100644 index 326bd87..0000000 --- a/cli/koji_cli/commands/cmd_add_target.py +++ /dev/null @@ -1,45 +0,0 @@ -from __future__ import absolute_import, division - -from optparse import OptionParser - - - -from koji_cli.lib import ( - activate_session, - error, - get_usage_str -) - - -def handle_add_target(goptions, session, args): - "[admin] Create a new build target" - usage = "usage: %prog add-target " - parser = OptionParser(usage=get_usage_str(usage)) - (options, args) = parser.parse_args(args) - if len(args) < 2: - parser.error("Please specify a target name, a build tag, and destination tag") - elif len(args) > 3: - parser.error("Incorrect number of arguments") - name = args[0] - build_tag = args[1] - if len(args) > 2: - dest_tag = args[2] - else: - # most targets have the same name as their destination - dest_tag = name - activate_session(session, goptions) - if not (session.hasPerm('admin') or session.hasPerm('target')): - parser.error("This action requires target or admin privileges") - - chkbuildtag = session.getTag(build_tag) - chkdesttag = session.getTag(dest_tag) - if not chkbuildtag: - error("No such tag: %s" % build_tag) - if not chkbuildtag.get("arches", None): - error("Build tag has no arches: %s" % build_tag) - if not chkdesttag: - error("No such destination tag: %s" % dest_tag) - - session.createBuildTarget(name, build_tag, dest_tag) - - diff --git a/cli/koji_cli/commands/cmd_add_user.py b/cli/koji_cli/commands/cmd_add_user.py deleted file mode 100644 index 99727c5..0000000 --- a/cli/koji_cli/commands/cmd_add_user.py +++ /dev/null @@ -1,34 +0,0 @@ -from __future__ import absolute_import, division - -from optparse import OptionParser - - -import koji - -from koji_cli.lib import ( - activate_session, - get_usage_str -) - - -def handle_add_user(goptions, session, args): - "[admin] Add a user" - usage = "usage: %prog add-user [options]" - parser = OptionParser(usage=get_usage_str(usage)) - parser.add_option("--principal", help="The Kerberos principal for this user") - parser.add_option("--disable", help="Prohibit logins by this user", action="store_true") - (options, args) = parser.parse_args(args) - if len(args) < 1: - parser.error("You must specify the username of the user to add") - elif len(args) > 1: - parser.error("This command only accepts one argument (username)") - username = args[0] - if options.disable: - status = koji.USER_STATUS['BLOCKED'] - else: - status = koji.USER_STATUS['NORMAL'] - activate_session(session, goptions) - user_id = session.createUser(username, status=status, krb_principal=options.principal) - print("Added user %s (%i)" % (username, user_id)) - - diff --git a/cli/koji_cli/commands/cmd_add_volume.py b/cli/koji_cli/commands/cmd_add_volume.py deleted file mode 100644 index 8d2e0c6..0000000 --- a/cli/koji_cli/commands/cmd_add_volume.py +++ /dev/null @@ -1,29 +0,0 @@ -from __future__ import absolute_import, division - -from optparse import OptionParser - - - -from koji_cli.lib import ( - activate_session, - error, - get_usage_str -) - - -def handle_add_volume(goptions, session, args): - "[admin] Add a new storage volume" - usage = "usage: %prog add-volume " - parser = OptionParser(usage=get_usage_str(usage)) - (options, args) = parser.parse_args(args) - if len(args) != 1: - parser.error("Command requires exactly one volume-name.") - name = args[0] - volinfo = session.getVolume(name) - if volinfo: - error("Volume %s already exists" % name) - activate_session(session, goptions) - volinfo = session.addVolume(name) - print("Added volume %(name)s with id %(id)i" % volinfo) - - diff --git a/cli/koji_cli/commands/cmd_assign_task.py b/cli/koji_cli/commands/cmd_assign_task.py deleted file mode 100644 index dd87426..0000000 --- a/cli/koji_cli/commands/cmd_assign_task.py +++ /dev/null @@ -1,50 +0,0 @@ -from __future__ import absolute_import, division - -from optparse import OptionParser - - -import koji - -from koji_cli.lib import ( - activate_session, - get_usage_str -) - - -def handle_assign_task(goptions, session, args): - "[admin] Assign a task to a host" - usage = 'usage: %prog assign-task ' - parser = OptionParser(usage=get_usage_str(usage)) - parser.add_option('-f', '--force', action='store_true', default=False, - help='force to assign a non-free task') - (options, args) = parser.parse_args(args) - - if len(args) != 2: - parser.error('please specify a task id and a hostname') - else: - task_id = int(args[0]) - hostname = args[1] - - taskinfo = session.getTaskInfo(task_id, request=False) - if taskinfo is None: - raise koji.GenericError("No such task: %s" % task_id) - - hostinfo = session.getHost(hostname) - if hostinfo is None: - raise koji.GenericError("No such host: %s" % hostname) - - force = False - if options.force: - force = True - - activate_session(session, goptions) - if not session.hasPerm('admin'): - parser.error("This action requires admin privileges") - - ret = session.assignTask(task_id, hostname, force) - if ret: - print('assigned task %d to host %s' % (task_id, hostname)) - else: - print('failed to assign task %d to host %s' % (task_id, hostname)) - - diff --git a/cli/koji_cli/commands/cmd_block_group.py b/cli/koji_cli/commands/cmd_block_group.py deleted file mode 100644 index 917f8fa..0000000 --- a/cli/koji_cli/commands/cmd_block_group.py +++ /dev/null @@ -1,39 +0,0 @@ -from __future__ import absolute_import, division - -from optparse import OptionParser - - - -from koji_cli.lib import ( - activate_session, - error, - get_usage_str -) - - -def handle_block_group(goptions, session, args): - "[admin] Block group in tag" - usage = "usage: %prog block-group " - parser = OptionParser(usage=get_usage_str(usage)) - (options, args) = parser.parse_args(args) - if len(args) != 2: - parser.error("Please specify a tag name and a group name") - tag = args[0] - group = args[1] - - activate_session(session, goptions) - if not (session.hasPerm('admin') or session.hasPerm('tag')): - parser.error("This action requires tag or admin privileges") - - dsttag = session.getTag(tag) - if not dsttag: - error("No such tag: %s" % tag) - - groups = dict([(p['name'], p['group_id']) for p in session.getTagGroups(tag, inherit=False)]) - group_id = groups.get(group, None) - if group_id is None: - error("Group %s doesn't exist within tag %s" % (group, tag)) - - session.groupListBlock(tag, group) - - diff --git a/cli/koji_cli/commands/cmd_block_group_pkg.py b/cli/koji_cli/commands/cmd_block_group_pkg.py deleted file mode 100644 index 08355c8..0000000 --- a/cli/koji_cli/commands/cmd_block_group_pkg.py +++ /dev/null @@ -1,28 +0,0 @@ -from __future__ import absolute_import, division - -from optparse import OptionParser - - - -from koji_cli.lib import ( - activate_session, - get_usage_str -) - - -def handle_block_group_pkg(goptions, session, args): - "[admin] Block a package from a group's package listing" - usage = "usage: %prog block-group-pkg [options] [ ...]" - usage += '\n' + "Note that blocking is propagated through the inheritance chain, so " \ - "it is not exactly the same as package removal." - parser = OptionParser(usage=get_usage_str(usage)) - (options, args) = parser.parse_args(args) - if len(args) < 3: - parser.error("You must specify a tag name, group name, and one or more package names") - tag = args[0] - group = args[1] - activate_session(session, goptions) - for pkg in args[2:]: - session.groupPackageListBlock(tag, group, pkg) - - diff --git a/cli/koji_cli/commands/cmd_block_group_req.py b/cli/koji_cli/commands/cmd_block_group_req.py deleted file mode 100644 index 7ecdf91..0000000 --- a/cli/koji_cli/commands/cmd_block_group_req.py +++ /dev/null @@ -1,26 +0,0 @@ -from __future__ import absolute_import, division - -from optparse import OptionParser - - - -from koji_cli.lib import ( - activate_session, - get_usage_str -) - - -def handle_block_group_req(goptions, session, args): - "[admin] Block a group's requirement listing" - usage = "usage: %prog block-group-req [options] " - parser = OptionParser(usage=get_usage_str(usage)) - (options, args) = parser.parse_args(args) - if len(args) != 3: - parser.error("You must specify a tag name and two group names") - tag = args[0] - group = args[1] - req = args[2] - activate_session(session, goptions) - session.groupReqListBlock(tag, group, req) - - diff --git a/cli/koji_cli/commands/cmd_block_notification.py b/cli/koji_cli/commands/cmd_block_notification.py deleted file mode 100644 index 598883a..0000000 --- a/cli/koji_cli/commands/cmd_block_notification.py +++ /dev/null @@ -1,65 +0,0 @@ -from __future__ import absolute_import, division - -from optparse import OptionParser - - -import koji - -from koji_cli.lib import ( - activate_session, - get_usage_str -) - - -def handle_block_notification(goptions, session, args): - "[monitor] Block user's notifications" - usage = "usage: %prog block-notification [options]" - parser = OptionParser(usage=get_usage_str(usage)) - parser.add_option("--user", help="Block notifications for this user (admin-only)") - parser.add_option("--package", help="Block notifications for this package") - parser.add_option("--tag", help="Block notifications for this tag") - parser.add_option("--all", action="store_true", help="Block all notification for this user") - (options, args) = parser.parse_args(args) - - if len(args) != 0: - parser.error("This command takes no arguments") - - if not options.package and not options.tag and not options.all: - parser.error("One of --tag, --package or --all must be specified.") - - activate_session(session, goptions) - - if options.user and not session.hasPerm('admin'): - parser.error("--user requires admin permission") - - if options.user: - user_id = session.getUser(options.user, strict=True)['id'] - else: - logged_in_user = session.getLoggedInUser() - if logged_in_user: - user_id = logged_in_user['id'] - else: - parser.error("Please login with authentication or specify --user") - - if options.package: - package_id = session.getPackageID(options.package) - if package_id is None: - parser.error("No such package: %s" % options.package) - else: - package_id = None - - if options.tag: - try: - tag_id = session.getTagID(options.tag, strict=True) - except koji.GenericError: - parser.error("No such tag: %s" % options.tag) - else: - tag_id = None - - for block in session.getBuildNotificationBlocks(user_id): - if block['package_id'] == package_id and block['tag_id'] == tag_id: - parser.error('Notification already exists.') - - session.createNotificationBlock(user_id, package_id, tag_id) - - diff --git a/cli/koji_cli/commands/cmd_block_pkg.py b/cli/koji_cli/commands/cmd_block_pkg.py deleted file mode 100644 index 49620d5..0000000 --- a/cli/koji_cli/commands/cmd_block_pkg.py +++ /dev/null @@ -1,55 +0,0 @@ -from __future__ import absolute_import, division - -from optparse import OptionParser - - -import koji - -from koji_cli.lib import ( - activate_session, - error, - get_usage_str, - warn -) - - -def handle_block_pkg(goptions, session, args): - "[admin] Block a package in the listing for tag" - usage = "usage: %prog block-pkg [options] [ ...]" - parser = OptionParser(usage=get_usage_str(usage)) - parser.add_option("--force", action='store_true', default=False, - help="Override blocks and owner if necessary") - (options, args) = parser.parse_args(args) - if len(args) < 2: - parser.error("Please specify a tag and at least one package") - activate_session(session, goptions) - tag = args[0] - # check if list of packages exists for that tag already - dsttag = session.getTag(tag) - if dsttag is None: - error("No such tag: %s" % tag) - try: - pkglist = session.listPackages(tagID=dsttag['id'], inherited=True, with_owners=False) - except koji.ParameterError: - # performance option added in 1.25 - pkglist = session.listPackages(tagID=dsttag['id'], inherited=True) - pkglist = dict([(p['package_name'], p['package_id']) for p in pkglist]) - ret = 0 - for package in args[1:]: - package_id = pkglist.get(package, None) - if package_id is None: - warn("Package %s doesn't exist in tag %s" % (package, tag)) - ret = 1 - if ret: - error(code=ret) - session.multicall = True - for package in args[1:]: - # force is not supported on older hub, so use it only explicitly - # https://pagure.io/koji/issue/1388 - if options.force: - session.packageListBlock(tag, package, force=options.force) - else: - session.packageListBlock(tag, package) - session.multiCall(strict=True) - - diff --git a/cli/koji_cli/commands/cmd_build.py b/cli/koji_cli/commands/cmd_build.py deleted file mode 100644 index 7f88230..0000000 --- a/cli/koji_cli/commands/cmd_build.py +++ /dev/null @@ -1,132 +0,0 @@ -from __future__ import absolute_import, division - -import json -import os -import textwrap -from optparse import OptionParser - - -import koji - -from koji_cli.lib import ( - _progress_callback, - _running_in_bg, - activate_session, - get_usage_str, - unique_path, - watch_tasks -) - - -def handle_build(options, session, args): - "[build] Build a package from source" - - usage = """\ - usage: %prog build [options] - - The first option is the build target, not to be confused with the destination - tag (where the build eventually lands) or build tag (where the buildroot - contents are pulled from). - - You can list all available build targets using the '%prog list-targets' command. - More detail can be found in the documentation. - https://docs.pagure.org/koji/HOWTO/#package-organization""" - - usage = textwrap.dedent(usage) - parser = OptionParser(usage=get_usage_str(usage)) - parser.add_option("--skip-tag", action="store_true", help="Do not attempt to tag package") - parser.add_option("--scratch", action="store_true", help="Perform a scratch build") - parser.add_option("--rebuild-srpm", action="store_true", dest="rebuild_srpm", - help="Force rebuilding SRPM for scratch build only") - parser.add_option("--no-rebuild-srpm", action="store_false", dest="rebuild_srpm", - help="Force not to rebuild srpm for scratch build only") - parser.add_option("--wait", action="store_true", - help="Wait on the build, even if running in the background") - parser.add_option("--nowait", action="store_false", dest="wait", help="Don't wait on build") - parser.add_option("--wait-repo", action="store_true", - help="Wait for the actual buildroot repo of given target") - parser.add_option("--wait-build", metavar="NVR", action="append", dest="wait_builds", - default=[], help="Wait for the given nvr to appear in buildroot repo") - parser.add_option("--quiet", action="store_true", - help="Do not print the task information", default=options.quiet) - parser.add_option("--arch-override", help="Override build arches") - parser.add_option("--fail-fast", action="store_true", - help="Override build_arch_can_fail settings and fail as fast as possible") - parser.add_option("--repo-id", type="int", help="Use a specific repo") - parser.add_option("--noprogress", action="store_true", - help="Do not display progress of the upload") - parser.add_option("--background", action="store_true", - help="Run the build at a lower priority") - parser.add_option("--custom-user-metadata", type="str", - help="Provide a JSON string of custom metadata to be deserialized and " - "stored under the build's extra.custom_user_metadata field") - (build_opts, args) = parser.parse_args(args) - if len(args) != 2: - parser.error("Exactly two arguments (a build target and a SCM URL or srpm file) are " - "required") - if build_opts.rebuild_srpm is not None and not build_opts.scratch: - parser.error("--no-/rebuild-srpm is only allowed for --scratch builds") - if build_opts.arch_override and not build_opts.scratch: - parser.error("--arch_override is only allowed for --scratch builds") - custom_user_metadata = {} - if build_opts.custom_user_metadata: - try: - custom_user_metadata = json.loads(build_opts.custom_user_metadata) - # Use ValueError instead of json.JSONDecodeError for Python 2 and 3 compatibility - except ValueError: - parser.error("--custom-user-metadata is not valid JSON") - - if not isinstance(custom_user_metadata, dict): - parser.error("--custom-user-metadata must be a JSON object") - - activate_session(session, options) - target = args[0] - if target.lower() == "none" and build_opts.repo_id: - target = None - build_opts.skip_tag = True - else: - build_target = session.getBuildTarget(target) - if not build_target: - parser.error("No such build target: %s" % target) - dest_tag = session.getTag(build_target['dest_tag']) - if not dest_tag: - parser.error("No such destination tag: %s" % build_target['dest_tag_name']) - if dest_tag['locked'] and not build_opts.scratch: - parser.error("Destination tag %s is locked" % dest_tag['name']) - source = args[1] - opts = {} - if build_opts.arch_override: - opts['arch_override'] = koji.parse_arches(build_opts.arch_override) - for key in ('skip_tag', 'scratch', 'repo_id', 'fail_fast', 'wait_repo', 'wait_builds', - 'rebuild_srpm'): - val = getattr(build_opts, key) - if val is not None: - opts[key] = val - opts["custom_user_metadata"] = custom_user_metadata - priority = None - if build_opts.background: - # relative to koji.PRIO_DEFAULT - priority = 5 - # try to check that source is an SRPM - if '://' not in source: - # treat source as an srpm and upload it - if not build_opts.quiet: - print("Uploading srpm: %s" % source) - serverdir = unique_path('cli-build') - if _running_in_bg() or build_opts.noprogress or build_opts.quiet: - callback = None - else: - callback = _progress_callback - session.uploadWrapper(source, serverdir, callback=callback) - print('') - source = "%s/%s" % (serverdir, os.path.basename(source)) - task_id = session.build(source, target, opts, priority=priority) - if not build_opts.quiet: - print("Created task: %d" % task_id) - print("Task info: %s/taskinfo?taskID=%s" % (options.weburl, task_id)) - if build_opts.wait or (build_opts.wait is None and not _running_in_bg()): - session.logout() - return watch_tasks(session, [task_id], quiet=build_opts.quiet, - poll_interval=options.poll_interval, topurl=options.topurl) - - diff --git a/cli/koji_cli/commands/cmd_buildinfo.py b/cli/koji_cli/commands/cmd_buildinfo.py deleted file mode 100644 index aada1d3..0000000 --- a/cli/koji_cli/commands/cmd_buildinfo.py +++ /dev/null @@ -1,119 +0,0 @@ -from __future__ import absolute_import, division - -import os -from optparse import OptionParser - - -import koji - -from koji_cli.lib import ( - ensure_connection, - error, - get_usage_str, - warn -) - - -def anon_handle_buildinfo(goptions, session, args): - "[info] Print basic information about a build" - usage = "usage: %prog buildinfo [options] [ ...]" - parser = OptionParser(usage=get_usage_str(usage)) - parser.add_option("--changelog", action="store_true", help="Show the changelog for the build") - (options, args) = parser.parse_args(args) - if len(args) < 1: - parser.error("Please specify a build") - ensure_connection(session, goptions) - error_hit = False - for build in args: - if build.isdigit(): - build = int(build) - info = session.getBuild(build) - if info is None: - warn("No such build: %s\n" % build) - error_hit = True - continue - task = None - if info['task_id']: - task = session.getTaskInfo(info['task_id'], request=True) - taglist = [] - for tag in session.listTags(build): - taglist.append(tag['name']) - info['arch'] = 'src' - info['state'] = koji.BUILD_STATES[info['state']] - print("BUILD: %(name)s-%(version)s-%(release)s [%(id)d]" % info) - print("State: %(state)s" % info) - if info['state'] == 'BUILDING': - print("Reserved by: %(cg_name)s" % info) - print("Built by: %(owner_name)s" % info) - source = info.get('source') - if source is not None: - print("Source: %s" % source) - if 'volume_name' in info: - print("Volume: %(volume_name)s" % info) - if task: - print("Task: %s %s" % (task['id'], koji.taskLabel(task))) - else: - print("Task: none") - print("Finished: %s" % koji.formatTimeLong(info['completion_ts'])) - maven_info = session.getMavenBuild(info['id']) - if maven_info: - print("Maven groupId: %s" % maven_info['group_id']) - print("Maven artifactId: %s" % maven_info['artifact_id']) - print("Maven version: %s" % maven_info['version']) - win_info = session.getWinBuild(info['id']) - if win_info: - print("Windows build platform: %s" % win_info['platform']) - print("Tags: %s" % ' '.join(taglist)) - if info.get('extra'): - print("Extra: %(extra)r" % info) - archives_seen = {} - maven_archives = session.listArchives(buildID=info['id'], type='maven') - if maven_archives: - print("Maven archives:") - for archive in maven_archives: - archives_seen.setdefault(archive['id'], 1) - print(os.path.join(koji.pathinfo.mavenbuild(info), - koji.pathinfo.mavenfile(archive))) - win_archives = session.listArchives(buildID=info['id'], type='win') - if win_archives: - print("Windows archives:") - for archive in win_archives: - archives_seen.setdefault(archive['id'], 1) - print(os.path.join(koji.pathinfo.winbuild(info), koji.pathinfo.winfile(archive))) - img_archives = session.listArchives(buildID=info['id'], type='image') - if img_archives: - print('Image archives:') - for archive in img_archives: - archives_seen.setdefault(archive['id'], 1) - print(os.path.join(koji.pathinfo.imagebuild(info), archive['filename'])) - archive_idx = {} - for archive in session.listArchives(buildID=info['id']): - if archive['id'] in archives_seen: - continue - archive_idx.setdefault(archive['btype'], []).append(archive) - for btype in archive_idx: - archives = archive_idx[btype] - print('%s Archives:' % btype.capitalize()) - for archive in archives: - print(os.path.join(koji.pathinfo.typedir(info, btype), archive['filename'])) - rpms = session.listRPMs(buildID=info['id']) - if rpms: - with session.multicall() as mc: - for rpm in rpms: - rpm['sigs'] = mc.queryRPMSigs(rpm['id']) - print("RPMs:") - for rpm in rpms: - line = os.path.join(koji.pathinfo.build(info), koji.pathinfo.rpm(rpm)) - keys = ', '.join(sorted([x['sigkey'] for x in rpm['sigs'].result if x['sigkey']])) - if keys: - line += '\tSignatures: %s' % keys - print(line) - if options.changelog: - changelog = session.getChangelogEntries(info['id']) - if changelog: - print("Changelog:") - print(koji.util.formatChangelog(changelog)) - if error_hit: - error() - - diff --git a/cli/koji_cli/commands/cmd_call.py b/cli/koji_cli/commands/cmd_call.py deleted file mode 100644 index 6b85740..0000000 --- a/cli/koji_cli/commands/cmd_call.py +++ /dev/null @@ -1,61 +0,0 @@ -from __future__ import absolute_import, division - -import ast -import json -import pprint -import textwrap -from optparse import OptionParser - - - -from koji_cli.lib import ( - DatetimeJSONEncoder, - activate_session, - arg_filter, - get_usage_str -) - - -def handle_call(goptions, session, args): - "Execute an arbitrary XML-RPC call" - usage = """\ - usage: %prog call [options] [ ...] - - Note, that you can use global option --noauth for anonymous calls here""" - usage = textwrap.dedent(usage) - parser = OptionParser(usage=get_usage_str(usage)) - parser.add_option("--python", action="store_true", help="Use python syntax for values") - parser.add_option("--kwargs", - help="Specify keyword arguments as a dictionary (implies --python)") - parser.add_option("--json-output", action="store_true", help="Use JSON syntax for output") - (options, args) = parser.parse_args(args) - if len(args) < 1: - parser.error("Please specify the name of the XML-RPC method") - if options.kwargs: - options.python = True - if options.python and ast is None: - parser.error("The ast module is required to read python syntax") - if options.json_output and json is None: - parser.error("The json module is required to output JSON syntax") - activate_session(session, goptions) - name = args[0] - non_kw = [] - kw = {} - if options.python: - non_kw = [ast.literal_eval(a) for a in args[1:]] - if options.kwargs: - kw = ast.literal_eval(options.kwargs) - else: - for arg in args[1:]: - if arg.find('=') != -1: - key, value = arg.split('=', 1) - kw[key] = arg_filter(value) - else: - non_kw.append(arg_filter(arg)) - response = getattr(session, name).__call__(*non_kw, **kw) - if options.json_output: - print(json.dumps(response, indent=2, separators=(',', ': '), cls=DatetimeJSONEncoder)) - else: - pprint.pprint(response) - - diff --git a/cli/koji_cli/commands/cmd_cancel.py b/cli/koji_cli/commands/cmd_cancel.py deleted file mode 100644 index 22989d0..0000000 --- a/cli/koji_cli/commands/cmd_cancel.py +++ /dev/null @@ -1,62 +0,0 @@ -from __future__ import absolute_import, division - -from optparse import OptionParser - - -import koji - -from koji_cli.lib import ( - activate_session, - get_usage_str, - warn -) - - -def handle_cancel(goptions, session, args): - "[build] Cancel tasks and/or builds" - usage = "usage: %prog cancel [options] [ ...]" - parser = OptionParser(usage=get_usage_str(usage)) - parser.add_option("--justone", action="store_true", help="Do not cancel subtasks") - parser.add_option("--full", action="store_true", help="Full cancellation (admin only)") - parser.add_option("--force", action="store_true", help="Allow subtasks with --full") - (options, args) = parser.parse_args(args) - if len(args) == 0: - parser.error("You must specify at least one task id or build") - activate_session(session, goptions) - tlist = [] - blist = [] - for arg in args: - try: - tlist.append(int(arg)) - except ValueError: - try: - koji.parse_NVR(arg) - blist.append(arg) - except koji.GenericError: - parser.error("please specify only task ids (integer) or builds (n-v-r)") - - results = [] - with session.multicall(strict=False, batch=100) as m: - if tlist: - opts = {} - remote_fn = m.cancelTask - if options.justone: - opts['recurse'] = False - elif options.full: - remote_fn = m.cancelTaskFull - if options.force: - opts['strict'] = False - for task_id in tlist: - results.append(remote_fn(task_id, **opts)) - for build in blist: - results.append(m.cancelBuild(build)) - - err = False - for r in results: - if isinstance(r.result, dict): - warn(r.result['faultString']) - err = True - if err: - return 1 - - diff --git a/cli/koji_cli/commands/cmd_chain_build.py b/cli/koji_cli/commands/cmd_chain_build.py deleted file mode 100644 index 045ddc2..0000000 --- a/cli/koji_cli/commands/cmd_chain_build.py +++ /dev/null @@ -1,90 +0,0 @@ -from __future__ import absolute_import, division - -from optparse import OptionParser - - - -from koji_cli.lib import ( - _running_in_bg, - activate_session, - error, - get_usage_str, - warn, - watch_tasks -) - - -def handle_chain_build(options, session, args): - # XXX - replace handle_build with this, once chain-building has gotten testing - "[build] Build one or more packages from source" - usage = "usage: %prog chain-build [options] [ [:] [:] ...]" - parser = OptionParser(usage=get_usage_str(usage)) - parser.add_option("--wait", action="store_true", - help="Wait on build, even if running in the background") - parser.add_option("--nowait", action="store_false", dest="wait", help="Don't wait on build") - parser.add_option("--quiet", action="store_true", - help="Do not print the task information", default=options.quiet) - parser.add_option("--background", action="store_true", - help="Run the build at a lower priority") - (build_opts, args) = parser.parse_args(args) - if len(args) < 2: - parser.error("At least two arguments (a build target and a SCM URL) are required") - activate_session(session, options) - target = args[0] - build_target = session.getBuildTarget(target) - if not build_target: - parser.error("No such build target: %s" % target) - dest_tag = session.getTag(build_target['dest_tag'], strict=True) - if dest_tag['locked']: - parser.error("Destination tag %s is locked" % dest_tag['name']) - - # check that the destination tag is in the inheritance tree of the build tag - # otherwise there is no way that a chain-build can work - ancestors = session.getFullInheritance(build_target['build_tag']) - if dest_tag['id'] not in [build_target['build_tag']] + \ - [ancestor['parent_id'] for ancestor in ancestors]: - warn("Packages in destination tag %(dest_tag_name)s are not inherited by build tag " - "%(build_tag_name)s" % build_target) - error("Target %s is not usable for a chain-build" % build_target['name']) - sources = args[1:] - - src_list = [] - build_level = [] - # src_lists is a list of lists of sources to build. - # each list is block of builds ("build level") which must all be completed - # before the next block begins. Blocks are separated on the command line with ':' - for src in sources: - if src == ':': - if build_level: - src_list.append(build_level) - build_level = [] - elif '://' in src: - # quick check that src might be a url - build_level.append(src) - elif '/' not in src and not src.endswith('.rpm') and len(src.split('-')) >= 3: - # quick check that it looks like a N-V-R - build_level.append(src) - else: - error('"%s" is not a SCM URL or package N-V-R' % src) - if build_level: - src_list.append(build_level) - - if len(src_list) < 2: - parser.error('You must specify at least one dependency between builds with : (colon)\n' - 'If there are no dependencies, use the build command instead') - - priority = None - if build_opts.background: - # relative to koji.PRIO_DEFAULT - priority = 5 - - task_id = session.chainBuild(src_list, target, priority=priority) - if not build_opts.quiet: - print("Created task: %d" % task_id) - print("Task info: %s/taskinfo?taskID=%s" % (options.weburl, task_id)) - if build_opts.wait or (build_opts.wait is None and not _running_in_bg()): - session.logout() - return watch_tasks(session, [task_id], quiet=build_opts.quiet, - poll_interval=options.poll_interval, topurl=options.topurl) - - diff --git a/cli/koji_cli/commands/cmd_clone_tag.py b/cli/koji_cli/commands/cmd_clone_tag.py deleted file mode 100644 index 5016bea..0000000 --- a/cli/koji_cli/commands/cmd_clone_tag.py +++ /dev/null @@ -1,539 +0,0 @@ -from __future__ import absolute_import, division - -import sys -import time -from collections import OrderedDict, defaultdict -from optparse import OptionParser - -import six -import six.moves.xmlrpc_client -from six.moves import zip - -import koji - -from koji_cli.lib import ( - activate_session, - error, - get_usage_str -) - - -def handle_clone_tag(goptions, session, args): - "[admin] Duplicate the contents of one tag onto another tag" - usage = "usage: %prog clone-tag [options] " - usage += "\nclone-tag will create the destination tag if it does not already exist" - parser = OptionParser(usage=get_usage_str(usage)) - parser.add_option('--config', action='store_true', - help="Copy config from the source to the dest tag") - parser.add_option('--groups', action='store_true', help="Copy group information") - parser.add_option('--pkgs', action='store_true', - help="Copy package list from the source to the dest tag") - parser.add_option('--builds', action='store_true', help="Tag builds into the dest tag") - parser.add_option('--all', action='store_true', - help="The same as --config --groups --pkgs --builds") - parser.add_option('--latest-only', action='store_true', - help="Tag only the latest build of each package") - parser.add_option('--inherit-builds', action='store_true', - help="Include all builds inherited into the source tag into the dest tag") - parser.add_option('--ts', type='int', metavar="TIMESTAMP", - help='Clone tag at last event before specific timestamp') - parser.add_option('--no-delete', action='store_false', dest="delete", default=True, - help="Don't delete any existing content in dest tag.") - parser.add_option('--event', type='int', help='Clone tag at a specific event') - parser.add_option('--repo', type='int', help='Clone tag at a specific repo event') - parser.add_option("-v", "--verbose", action="store_true", help="show changes") - parser.add_option("--notify", action="store_true", default=False, - help='Send tagging/untagging notifications') - parser.add_option("-f", "--force", action="store_true", - help="override tag locks if necessary") - parser.add_option("-n", "--test", action="store_true", help="test mode") - parser.add_option("--batch", type='int', default=100, metavar='SIZE', - help="batch size of multicalls [0 to disable, default: %default]") - (options, args) = parser.parse_args(args) - - if len(args) != 2: - parser.error("This command takes two arguments: ") - activate_session(session, goptions) - - if not options.test and not (session.hasPerm('admin') or session.hasPerm('tag')): - parser.error("This action requires tag or admin privileges") - - if args[0] == args[1]: - parser.error('Source and destination tags must be different.') - - if options.batch < 0: - parser.error("batch size must be bigger than zero") - - if options.all: - options.config = options.groups = options.pkgs = options.builds = True - - event = koji.util.eventFromOpts(session, options) or {} - if event: - event['timestr'] = time.asctime(time.localtime(event['ts'])) - print("Cloning at event %(id)i (%(timestr)s)" % event) - - # store tags. - try: - srctag = session.getBuildConfig(args[0], event=event.get('id')) - except koji.GenericError: - parser.error("No such src-tag: %s" % args[0]) - dsttag = session.getTag(args[1]) - if not srctag: - parser.error("No such src-tag: %s" % args[0]) - if (srctag['locked'] and not options.force) \ - or (dsttag and dsttag['locked'] and not options.force): - parser.error("Error: You are attempting to clone from or to a tag which is locked.\n" - "Please use --force if this is what you really want to do.") - - # init debug lists. - chgpkglist = [] - chgbldlist = [] - chggrplist = [] - # case of brand new dst-tag. - if not dsttag: - # create a new tag, copy srctag header. - if not options.test: - if options.config: - session.createTag(args[1], parent=None, arches=srctag['arches'], - perm=srctag['perm_id'], - locked=srctag['locked'], - maven_support=srctag['maven_support'], - maven_include_all=srctag['maven_include_all'], - extra=srctag['extra']) - else: - session.createTag(args[1], parent=None) - # store the new tag, need its assigned id. - newtag = session.getTag(args[1], strict=True) - # get pkglist of src-tag, including inherited packages. - if options.pkgs: - srcpkgs = session.listPackages(tagID=srctag['id'], - inherited=True, - event=event.get('id')) - srcpkgs.sort(key=lambda x: x['package_name']) - if not options.test: - session.multicall = True - for pkgs in srcpkgs: - # for each package add one entry in the new tag. - chgpkglist.append(('[new]', - pkgs['package_name'], - pkgs['blocked'], - pkgs['owner_name'], - pkgs['tag_name'])) - if not options.test: - # add packages. - session.packageListAdd(newtag['name'], - pkgs['package_name'], - owner=pkgs['owner_name'], - block=pkgs['blocked'], - extra_arches=pkgs['extra_arches']) - if not options.test: - _multicall_with_check(session, options.batch) - if options.builds: - # get --all latest builds from src tag - builds = reversed(session.listTagged(srctag['id'], - event=event.get('id'), - inherit=options.inherit_builds, - latest=options.latest_only)) - if not options.test: - session.multicall = True - for build in builds: - # add missing 'name' field. - build['name'] = build['package_name'] - chgbldlist.append(('[new]', - build['package_name'], - build['nvr'], - koji.BUILD_STATES[build['state']], - build['owner_name'], - build['tag_name'])) - # copy latest builds into new tag - if not options.test: - session.tagBuildBypass(newtag['name'], - build, - force=options.force, - notify=options.notify) - if not options.test: - _multicall_with_check(session, options.batch) - if options.groups: - # Copy the group data - srcgroups = session.getTagGroups(srctag['name'], - event=event.get('id')) - if not options.test: - session.multicall = True - for group in srcgroups: - if not options.test: - session.groupListAdd(newtag['name'], group['name']) - for pkg in group['packagelist']: - if not options.test: - session.groupPackageListAdd(newtag['name'], - group['name'], - pkg['package'], - block=pkg['blocked']) - chggrplist.append(('[new]', pkg['package'], group['name'])) - if not options.test: - _multicall_with_check(session, options.batch) - # case of existing dst-tag. - if dsttag: - if options.config and not options.test: - if dsttag['extra']: - session.editTag2(dsttag['id'], remove_extra=list(dsttag['extra'].keys())) - session.editTag2(dsttag['id'], parent=None, arches=srctag['arches'], - perm=srctag['perm_id'], - locked=srctag['locked'], - maven_support=srctag['maven_support'], - maven_include_all=srctag['maven_include_all'], - extra=srctag['extra']) - dsttag = session.getTag(dsttag['id'], strict=True) - - # get fresh list of packages & builds into maps. - srcpkgs = {} - dstpkgs = {} - srcbldsbypkg = defaultdict(OrderedDict) - dstbldsbypkg = defaultdict(OrderedDict) - srcgroups = OrderedDict() - dstgroups = OrderedDict() - # we use OrderedDict so that these indexes preserve the order given to us - if options.pkgs: - for pkg in session.listPackages(tagID=srctag['id'], - inherited=True, - event=event.get('id')): - srcpkgs[pkg['package_name']] = pkg - for pkg in session.listPackages(tagID=dsttag['id'], - inherited=True): - dstpkgs[pkg['package_name']] = pkg - if options.builds: - # listTagged orders builds latest-first - # so reversing that gives us oldest-first - for build in reversed(session.listTagged(srctag['id'], - event=event.get('id'), - inherit=options.inherit_builds, - latest=options.latest_only)): - srcbldsbypkg[build['package_name']][build['nvr']] = build - # get builds in dsttag without inheritance. - # latest=False to get all builds even when latest_only = True, - # so that only the *latest* build per tag will live in. - for build in reversed(session.listTagged(dsttag['id'], - inherit=False, - latest=False)): - dstbldsbypkg[build['package_name']][build['nvr']] = build - if options.groups: - for group in session.getTagGroups(srctag['name'], - event=event.get('id')): - srcgroups[group['name']] = group - for group in session.getTagGroups(dsttag['name']): - dstgroups[group['name']] = group - # construct to-do lists. - paddlist = [] # list containing new packages to be added from src tag - for (package_name, pkg) in six.iteritems(srcpkgs): - if package_name not in dstpkgs: - paddlist.append(pkg) - paddlist.sort(key=lambda x: x['package_name']) - pdellist = [] # list containing packages no more present in dst tag - for (package_name, pkg) in six.iteritems(dstpkgs): - if package_name not in srcpkgs: - pdellist.append(pkg) - pdellist.sort(key=lambda x: x['package_name']) - baddlist = [] # list containing new builds to be added from src tag - bdellist = [] # list containing new builds to be removed from dst tag - if options.delete: - # remove builds for packages that are absent from src tag - for (pkg, dstblds) in six.iteritems(dstbldsbypkg): - if pkg not in srcbldsbypkg: - bdellist.extend(dstblds.values()) - # add and/or remove builds from dst to match src contents and order - for (pkg, srcblds) in six.iteritems(srcbldsbypkg): - dstblds = dstbldsbypkg[pkg] - ablds = [] - dblds = [] - # firstly, deal with extra builds in dst - removed_nvrs = set(dstblds.keys()) - set(srcblds.keys()) - bld_order = srcblds.copy() - if options.delete: - # mark the extra builds for deletion - dnvrs = [] - for (dstnvr, dstbld) in six.iteritems(dstblds): - if dstnvr in removed_nvrs: - dnvrs.append(dstnvr) - dblds.append(dstbld) - # we also remove them from dstblds now so that they do not - # interfere with the order comparison below - for dnvr in dnvrs: - del dstblds[dnvr] - else: - # in the no-delete case, the extra builds should be forced - # to last in the tag - bld_order = OrderedDict() - for (dstnvr, dstbld) in six.iteritems(dstblds): - if dstnvr in removed_nvrs: - bld_order[dstnvr] = dstbld - for (nvr, srcbld) in six.iteritems(srcblds): - bld_order[nvr] = srcbld - # secondly, add builds from src tag and adjust the order - for (nvr, srcbld) in six.iteritems(bld_order): - found = False - out_of_order = [] - # note that dstblds is trimmed as we go, so we are only - # considering the tail corresponding to where we are at - # in the srcblds loop - for (dstnvr, dstbld) in six.iteritems(dstblds): - if nvr == dstnvr: - found = True - break - else: - out_of_order.append(dstnvr) - dblds.append(dstbld) - for dnvr in out_of_order: - del dstblds[dnvr] - # these will be re-added in the proper order later - if found: - # remove it for next pass so we stay aligned with outer - # loop - del dstblds[nvr] - else: - # missing from dst, so we need to add it - ablds.append(srcbld) - baddlist.extend(ablds) - bdellist.extend(dblds) - baddlist.sort(key=lambda x: x['package_name']) - bdellist.sort(key=lambda x: x['package_name']) - - gaddlist = [] # list containing new groups to be added from src tag - for (grpname, group) in six.iteritems(srcgroups): - if grpname not in dstgroups: - gaddlist.append(group) - gdellist = [] # list containing groups to be removed from src tag - for (grpname, group) in six.iteritems(dstgroups): - if grpname not in srcgroups: - gdellist.append(group) - grpchanges = OrderedDict() # dict of changes to make in shared groups - for (grpname, group) in six.iteritems(srcgroups): - if grpname in dstgroups: - dstgroup = dstgroups[grpname] - grpchanges[grpname] = {'adds': [], 'dels': []} - # Store whether group is inherited or not - grpchanges[grpname]['inherited'] = False - if dstgroup['tag_id'] != dsttag['id']: - grpchanges[grpname]['inherited'] = True - srcgrppkglist = [] - dstgrppkglist = [] - for pkg in group['packagelist']: - srcgrppkglist.append(pkg['package']) - for pkg in dstgroups[grpname]['packagelist']: - dstgrppkglist.append(pkg['package']) - for pkg in srcgrppkglist: - if pkg not in dstgrppkglist: - grpchanges[grpname]['adds'].append(pkg) - for pkg in dstgrppkglist: - if pkg not in srcgrppkglist: - grpchanges[grpname]['dels'].append(pkg) - # ADD new packages. - if not options.test: - session.multicall = True - for pkg in paddlist: - chgpkglist.append(('[add]', - pkg['package_name'], - pkg['blocked'], - pkg['owner_name'], - pkg['tag_name'])) - if not options.test: - session.packageListAdd(dsttag['name'], - pkg['package_name'], - owner=pkg['owner_name'], - block=pkg['blocked'], - extra_arches=pkg['extra_arches']) - if not options.test: - _multicall_with_check(session, options.batch) - # DEL builds. To keep the order we should untag builds at first - if not options.test: - session.multicall = True - for build in bdellist: - # don't delete an inherited build. - if build['tag_name'] == dsttag['name']: - # add missing 'name' field - build['name'] = build['package_name'] - chgbldlist.append(('[del]', - build['package_name'], - build['nvr'], - koji.BUILD_STATES[build['state']], - build['owner_name'], - build['tag_name'])) - # go on del builds from new tag. - if not options.test: - session.untagBuildBypass(dsttag['name'], - build, - force=options.force, - notify=options.notify) - if not options.test: - _multicall_with_check(session, options.batch) - # ADD builds. - if not options.test: - session.multicall = True - for build in baddlist: - # add missing 'name' field. - build['name'] = build['package_name'] - chgbldlist.append(('[add]', - build['package_name'], - build['nvr'], - koji.BUILD_STATES[build['state']], - build['owner_name'], - build['tag_name'])) - # copy latest builds into new tag. - if not options.test: - session.tagBuildBypass(dsttag['name'], - build, - force=options.force, - notify=options.notify) - if not options.test: - _multicall_with_check(session, options.batch) - # ADD groups. - if not options.test: - session.multicall = True - for group in gaddlist: - if not options.test: - session.groupListAdd(dsttag['name'], - group['name'], - force=options.force) - for pkg in group['packagelist']: - if not options.test: - session.groupPackageListAdd(dsttag['name'], - group['name'], - pkg['package'], - force=options.force) - chggrplist.append(('[new]', pkg['package'], group['name'])) - if not options.test: - _multicall_with_check(session, options.batch) - # ADD group pkgs. - if not options.test: - session.multicall = True - for group in grpchanges: - for pkg in grpchanges[group]['adds']: - chggrplist.append(('[new]', pkg, group)) - if not options.test: - session.groupPackageListAdd(dsttag['name'], - group, - pkg, - force=options.force) - if not options.test: - _multicall_with_check(session, options.batch) - if options.delete: - # DEL packages - ninhrtpdellist = [] - inhrtpdellist = [] - for pkg in pdellist: - if pkg['tag_name'] == dsttag['name']: - ninhrtpdellist.append(pkg) - else: - inhrtpdellist.append(pkg) - session.multicall = True - # delete only non-inherited packages. - for pkg in ninhrtpdellist: - # check if package have owned builds inside. - session.listTagged(dsttag['name'], - package=pkg['package_name'], - inherit=False) - bump_builds = session.multiCall(batch=options.batch) - if not options.test: - session.multicall = True - for pkg, [builds] in zip(ninhrtpdellist, bump_builds): - if isinstance(builds, dict): - error(builds['faultString']) - # remove all its builds first if there are any. - for build in builds: - # add missing 'name' field. - build['name'] = build['package_name'] - chgbldlist.append(('[del]', - build['package_name'], - build['nvr'], - koji.BUILD_STATES[build['state']], - build['owner_name'], - build['tag_name'])) - # so delete latest build(s) from new tag. - if not options.test: - session.untagBuildBypass(dsttag['name'], - build, - force=options.force, - notify=options.notify) - # now safe to remove package itself since we resolved its builds. - chgpkglist.append(('[del]', - pkg['package_name'], - pkg['blocked'], - pkg['owner_name'], - pkg['tag_name'])) - if not options.test: - session.packageListRemove(dsttag['name'], - pkg['package_name'], - force=False) - # mark as blocked inherited packages. - for pkg in inhrtpdellist: - chgpkglist.append(('[blk]', - pkg['package_name'], - pkg['blocked'], - pkg['owner_name'], - pkg['tag_name'])) - if not options.test: - session.packageListBlock(dsttag['name'], pkg['package_name']) - if not options.test: - _multicall_with_check(session, options.batch) - # DEL groups. - if not options.test: - session.multicall = True - for group in gdellist: - # Only delete a group that isn't inherited - if group['tag_id'] == dsttag['id']: - if not options.test: - session.groupListRemove(dsttag['name'], - group['name'], - force=options.force) - for pkg in group['packagelist']: - chggrplist.append(('[del]', pkg['package'], group['name'])) - # mark as blocked inherited groups. - else: - if not options.test: - session.groupListBlock(dsttag['name'], group['name']) - for pkg in group['packagelist']: - chggrplist.append(('[blk]', pkg['package'], group['name'])) - if not options.test: - _multicall_with_check(session, options.batch) - # DEL group pkgs. - if not options.test: - session.multicall = True - for group in grpchanges: - for pkg in grpchanges[group]['dels']: - # Only delete a group that isn't inherited - if not grpchanges[group]['inherited']: - chggrplist.append(('[del]', pkg, group)) - if not options.test: - session.groupPackageListRemove(dsttag['name'], - group, - pkg, - force=options.force) - else: - chggrplist.append(('[blk]', pkg, group)) - if not options.test: - session.groupPackageListBlock(dsttag['name'], - group, - pkg) - if not options.test: - _multicall_with_check(session, options.batch) - # print final list of actions. - if options.verbose: - pfmt = ' %-7s %-28s %-10s %-10s %-10s\n' - bfmt = ' %-7s %-28s %-40s %-10s %-10s %-10s\n' - gfmt = ' %-7s %-28s %-28s\n' - sys.stdout.write('\nList of changes:\n\n') - sys.stdout.write(pfmt % ('Action', 'Package', 'Blocked', 'Owner', 'From Tag')) - sys.stdout.write(pfmt % ('-' * 7, '-' * 28, '-' * 10, '-' * 10, '-' * 10)) - for changes in chgpkglist: - sys.stdout.write(pfmt % changes) - sys.stdout.write('\n') - sys.stdout.write(bfmt % - ('Action', 'From/To Package', 'Build(s)', 'State', 'Owner', 'From Tag')) - sys.stdout.write(bfmt % ('-' * 7, '-' * 28, '-' * 40, '-' * 10, '-' * 10, '-' * 10)) - for changes in chgbldlist: - sys.stdout.write(bfmt % changes) - sys.stdout.write('\n') - sys.stdout.write(gfmt % ('Action', 'Package', 'Group')) - sys.stdout.write(gfmt % ('-' * 7, '-' * 28, '-' * 28)) - for changes in chggrplist: - sys.stdout.write(gfmt % changes) - - diff --git a/cli/koji_cli/commands/cmd_disable_channel.py b/cli/koji_cli/commands/cmd_disable_channel.py deleted file mode 100644 index f16bd70..0000000 --- a/cli/koji_cli/commands/cmd_disable_channel.py +++ /dev/null @@ -1,39 +0,0 @@ -from __future__ import absolute_import, division - -from optparse import OptionParser - -from six.moves import zip - - -from koji_cli.lib import ( - activate_session, - error, - get_usage_str -) - - -def handle_disable_channel(goptions, session, args): - "[admin] Mark one or more channels as disabled" - usage = "usage: %prog disable-channel [options] [ ...]" - parser = OptionParser(usage=get_usage_str(usage)) - parser.add_option("--comment", help="Comment indicating why the channel(s) are being disabled") - (options, args) = parser.parse_args(args) - - if not args: - parser.error("At least one channel must be specified") - - activate_session(session, goptions) - - with session.multicall() as m: - result = [m.getChannel(channel, strict=False) for channel in args] - error_hit = False - for channel, id in zip(args, result): - if not id.result: - print("No such channel: %s" % channel) - error_hit = True - if error_hit: - error("No changes made. Please correct the command line.") - with session.multicall() as m: - [m.disableChannel(channel, comment=options.comment) for channel in args] - - diff --git a/cli/koji_cli/commands/cmd_disable_host.py b/cli/koji_cli/commands/cmd_disable_host.py deleted file mode 100644 index 9d64819..0000000 --- a/cli/koji_cli/commands/cmd_disable_host.py +++ /dev/null @@ -1,43 +0,0 @@ -from __future__ import absolute_import, division - -from optparse import OptionParser - -from six.moves import zip - - -from koji_cli.lib import ( - activate_session, - error, - get_usage_str -) - - -def handle_disable_host(goptions, session, args): - "[admin] Mark one or more hosts as disabled" - usage = "usage: %prog disable-host [options] [ ...]" - parser = OptionParser(usage=get_usage_str(usage)) - parser.add_option("--comment", help="Comment indicating why the host(s) are being disabled") - (options, args) = parser.parse_args(args) - - if not args: - parser.error("At least one host must be specified") - - activate_session(session, goptions) - session.multicall = True - for host in args: - session.getHost(host) - error_hit = False - for host, [id] in zip(args, session.multiCall(strict=True)): - if not id: - print("No such host: %s" % host) - error_hit = True - if error_hit: - error("No changes made. Please correct the command line.") - session.multicall = True - for host in args: - session.disableHost(host) - if options.comment is not None: - session.editHost(host, comment=options.comment) - session.multiCall(strict=True) - - diff --git a/cli/koji_cli/commands/cmd_disable_user.py b/cli/koji_cli/commands/cmd_disable_user.py deleted file mode 100644 index 19217aa..0000000 --- a/cli/koji_cli/commands/cmd_disable_user.py +++ /dev/null @@ -1,26 +0,0 @@ -from __future__ import absolute_import, division - -from optparse import OptionParser - - - -from koji_cli.lib import ( - activate_session, - get_usage_str -) - - -def handle_disable_user(goptions, session, args): - "[admin] Disable logins by a user" - usage = "usage: %prog disable-user " - parser = OptionParser(usage=get_usage_str(usage)) - (options, args) = parser.parse_args(args) - if len(args) < 1: - parser.error("You must specify the username of the user to disable") - elif len(args) > 1: - parser.error("This command only accepts one argument (username)") - username = args[0] - activate_session(session, goptions) - session.disableUser(username) - - diff --git a/cli/koji_cli/commands/cmd_dist_repo.py b/cli/koji_cli/commands/cmd_dist_repo.py deleted file mode 100644 index e815f61..0000000 --- a/cli/koji_cli/commands/cmd_dist_repo.py +++ /dev/null @@ -1,167 +0,0 @@ -from __future__ import absolute_import, division - -import os -from optparse import OptionParser - - -import koji - -from koji_cli.lib import ( - _progress_callback, - _running_in_bg, - activate_session, - unique_path, - warn, - watch_tasks -) - - -def handle_dist_repo(options, session, args): - """Create a yum repo with distribution options""" - usage = "usage: %prog dist-repo [options] [ ...]\n\n" \ - "In normal mode, dist-repo behaves like any other koji task.\n" \ - "Sometimes you want to limit running distRepo tasks per tag to only\n" \ - "one. For such behaviour admin (with 'tag' permission) needs to\n" \ - "modify given tag's extra field 'distrepo.cancel_others' to True'\n" \ - "via 'koji edit-tag -x distrepo.cancel_others=True'\n" - usage += "\n(Specify the --help option for a list of other options)" - parser = OptionParser(usage=usage) - parser.add_option('--allow-missing-signatures', action='store_true', - default=False, - help='For RPMs not signed with a desired key, fall back to the primary copy') - parser.add_option("-a", "--arch", action='append', default=[], - help="Indicate an architecture to consider. The default is all " - "architectures associated with the given tag. This option may " - "be specified multiple times.") - parser.add_option("--with-src", action='store_true', help='Also generate a src repo') - parser.add_option("--split-debuginfo", action='store_true', default=False, - help='Split debuginfo info a separate repo for each arch') - parser.add_option('--comps', help='Include a comps file in the repodata') - parser.add_option('--delta-rpms', metavar='REPO', default=[], action='append', - help='Create delta rpms. REPO can be the id of another dist repo ' - 'or the name of a tag that has a dist repo. May be specified ' - 'multiple times.') - parser.add_option('--event', type='int', help='Use tag content at event') - parser.add_option("--volume", help="Generate repo on given volume") - parser.add_option('--non-latest', dest='latest', default=True, - action='store_false', help='Include older builds, not just the latest') - parser.add_option('--multilib', default=None, metavar="CONFIG", - help='Include multilib packages in the repository using the given ' - 'config file') - parser.add_option("--noinherit", action='store_true', default=False, - help='Do not consider tag inheritance') - parser.add_option("--wait", action="store_true", - help="Wait for the task to complete, even if running in the background") - parser.add_option("--nowait", action="store_false", dest="wait", - help="Do not wait for the task to complete") - parser.add_option('--skip-missing-signatures', action='store_true', default=False, - help='Skip RPMs not signed with the desired key(s)') - parser.add_option('--zck', action='store_true', default=False, - help='Generate zchunk files as well as the standard repodata') - parser.add_option('--zck-dict-dir', action='store', default=None, - help='Directory containing compression dictionaries for use by zchunk ' - '(on builder)') - parser.add_option("--write-signed-rpms", action='store_true', default=False, - help='Write a signed rpms for given tag') - task_opts, args = parser.parse_args(args) - if len(args) < 1: - parser.error('You must provide a tag to generate the repo from') - if len(args) < 2 and not task_opts.allow_missing_signatures: - parser.error('Please specify one or more GPG key IDs (or --allow-missing-signatures)') - if task_opts.allow_missing_signatures and task_opts.skip_missing_signatures: - parser.error('allow_missing_signatures and skip_missing_signatures are mutually exclusive') - activate_session(session, options) - stuffdir = unique_path('cli-dist-repo') - if task_opts.comps: - if not os.path.exists(task_opts.comps): - parser.error('could not find %s' % task_opts.comps) - session.uploadWrapper(task_opts.comps, stuffdir, - callback=_progress_callback) - print('') - task_opts.comps = os.path.join(stuffdir, - os.path.basename(task_opts.comps)) - old_repos = [] - if len(task_opts.delta_rpms) > 0: - for repo in task_opts.delta_rpms: - if repo.isdigit(): - rinfo = session.repoInfo(int(repo), strict=True) - else: - # get dist repo for tag - rinfo = session.getRepo(repo, dist=True) - if not rinfo: - # maybe there is an expired one - rinfo = session.getRepo(repo, - state=koji.REPO_STATES['EXPIRED'], dist=True) - if not rinfo: - parser.error("Can't find repo for tag: %s" % repo) - old_repos.append(rinfo['id']) - tag = args[0] - keys = args[1:] - taginfo = session.getTag(tag) - if not taginfo: - parser.error('No such tag: %s' % tag) - allowed_arches = taginfo['arches'] or '' - if not allowed_arches: - for tag_inh in session.getFullInheritance(tag): - allowed_arches = session.getTag(tag_inh['parent_id'])['arches'] or '' - if allowed_arches: - break - if len(task_opts.arch) == 0: - task_opts.arch = allowed_arches.split() - if not task_opts.arch: - parser.error('No arches given and no arches associated with tag') - else: - for a in task_opts.arch: - if not allowed_arches: - warn('Tag %s has an empty arch list' % taginfo['name']) - elif a not in allowed_arches: - warn('%s is not in the list of tag arches' % a) - if task_opts.multilib: - if not os.path.exists(task_opts.multilib): - parser.error('could not find %s' % task_opts.multilib) - if 'x86_64' in task_opts.arch and 'i686' not in task_opts.arch: - parser.error('The multilib arch (i686) must be included') - if 's390x' in task_opts.arch and 's390' not in task_opts.arch: - parser.error('The multilib arch (s390) must be included') - if 'ppc64' in task_opts.arch and 'ppc' not in task_opts.arch: - parser.error('The multilib arch (ppc) must be included') - session.uploadWrapper(task_opts.multilib, stuffdir, - callback=_progress_callback) - task_opts.multilib = os.path.join(stuffdir, - os.path.basename(task_opts.multilib)) - print('') - if 'noarch' in task_opts.arch: - task_opts.arch.remove('noarch') - if task_opts.with_src and 'src' not in task_opts.arch: - task_opts.arch.append('src') - if not task_opts.arch: - parser.error('No arches left.') - - opts = { - 'arch': task_opts.arch, - 'comps': task_opts.comps, - 'delta': old_repos, - 'event': task_opts.event, - 'volume': task_opts.volume, - 'inherit': not task_opts.noinherit, - 'latest': task_opts.latest, - 'multilib': task_opts.multilib, - 'split_debuginfo': task_opts.split_debuginfo, - 'skip_missing_signatures': task_opts.skip_missing_signatures, - 'allow_missing_signatures': task_opts.allow_missing_signatures, - 'zck': task_opts.zck, - 'zck_dict_dir': task_opts.zck_dict_dir, - 'write_signed_rpms': task_opts.write_signed_rpms, - } - task_id = session.distRepo(tag, keys, **opts) - print("Creating dist repo for tag " + tag) - if task_opts.wait or (task_opts.wait is None and not _running_in_bg()): - session.logout() - return watch_tasks(session, [task_id], quiet=options.quiet, - poll_interval=options.poll_interval, topurl=options.topurl) - - -_search_types = ('package', 'build', 'tag', 'target', 'user', 'host', 'rpm', - 'maven', 'win') - - diff --git a/cli/koji_cli/commands/cmd_download_build.py b/cli/koji_cli/commands/cmd_download_build.py deleted file mode 100644 index ee83ed4..0000000 --- a/cli/koji_cli/commands/cmd_download_build.py +++ /dev/null @@ -1,132 +0,0 @@ -from __future__ import absolute_import, division - -from optparse import OptionParser - -from six.moves import zip - -import koji - -from koji_cli.lib import ( - download_archive, - download_rpm, - ensure_connection, - error, - get_usage_str, - warn -) - - -def anon_handle_download_build(options, session, args): - "[download] Download a built package" - usage = "usage: %prog download-build [options] " - parser = OptionParser(usage=get_usage_str(usage)) - parser.add_option("--arch", "-a", dest="arches", metavar="ARCH", action="append", default=[], - help="Only download packages for this arch (may be used multiple times)") - parser.add_option("--type", - help="Download archives of the given type, rather than rpms " - "(maven, win, image, remote-sources)") - parser.add_option("--latestfrom", dest="latestfrom", - help="Download the latest build from this tag") - parser.add_option("--debuginfo", action="store_true", help="Also download -debuginfo rpms") - parser.add_option("--task-id", action="store_true", help="Interperet id as a task id") - parser.add_option("--rpm", action="store_true", help="Download the given rpm") - parser.add_option("--key", help="Download rpms signed with the given key") - parser.add_option("--topurl", metavar="URL", default=options.topurl, - help="URL under which Koji files are accessible") - parser.add_option("--noprogress", action="store_true", help="Do not display progress meter") - parser.add_option("-q", "--quiet", action="store_true", - help="Suppress output", default=options.quiet) - (suboptions, args) = parser.parse_args(args) - if len(args) < 1: - parser.error("Please specify a package N-V-R or build ID") - elif len(args) > 1: - parser.error("Only a single package N-V-R or build ID may be specified") - - ensure_connection(session, options) - build = args[0] - - if build.isdigit(): - if suboptions.latestfrom: - parser.error("--latestfrom not compatible with build IDs, specify a package name.") - build = int(build) - if suboptions.task_id: - builds = session.listBuilds(taskID=build) - if not builds: - error("No associated builds for task %s" % build) - build = builds[0]['build_id'] - - if suboptions.latestfrom: - # We want the latest build, not a specific build - try: - builds = session.listTagged(suboptions.latestfrom, latest=True, package=build, - type=suboptions.type) - except koji.GenericError as data: - error("Error finding latest build: %s" % data) - if not builds: - error("%s has no builds of %s" % (suboptions.latestfrom, build)) - info = builds[0] - elif suboptions.rpm: - rpminfo = session.getRPM(build) - if rpminfo is None: - error("No such rpm: %s" % build) - info = session.getBuild(rpminfo['build_id']) - else: - # if we're given an rpm name without --rpm, download the containing build - try: - koji.parse_NVRA(build) - rpminfo = session.getRPM(build) - build = rpminfo['build_id'] - except Exception: - pass - info = session.getBuild(build) - - if info is None: - error("No such build: %s" % build) - - if not suboptions.topurl: - error("You must specify --topurl to download files") - - archives = [] - rpms = [] - if suboptions.type: - archives = session.listArchives(buildID=info['id'], type=suboptions.type) - if not archives: - error("No %s archives available for %s" % (suboptions.type, koji.buildLabel(info))) - else: - arches = suboptions.arches - if len(arches) == 0: - arches = None - if suboptions.rpm: - all_rpms = [rpminfo] - else: - all_rpms = session.listRPMs(buildID=info['id'], arches=arches) - if not all_rpms: - if arches: - error("No %s packages available for %s" % - (" or ".join(arches), koji.buildLabel(info))) - else: - error("No packages available for %s" % koji.buildLabel(info)) - for rpm in all_rpms: - if not suboptions.debuginfo and koji.is_debuginfo(rpm['name']): - continue - rpms.append(rpm) - - if suboptions.key: - with session.multicall() as m: - results = [m.queryRPMSigs(rpm_id=r['id'], sigkey=suboptions.key) for r in rpms] - rpm_keys = [x.result for x in results] - for rpm, rpm_key in list(zip(rpms, rpm_keys)): - if not rpm_key: - nvra = "%(nvr)s-%(arch)s.rpm" % rpm - warn("No such sigkey %s for rpm %s" % (suboptions.key, nvra)) - rpms.remove(rpm) - - # run the download - for rpm in rpms: - download_rpm(info, rpm, suboptions.topurl, sigkey=suboptions.key, - quiet=suboptions.quiet, noprogress=suboptions.noprogress) - for archive in archives: - download_archive(info, archive, suboptions.topurl, - quiet=suboptions.quiet, noprogress=suboptions.noprogress) - - diff --git a/cli/koji_cli/commands/cmd_download_logs.py b/cli/koji_cli/commands/cmd_download_logs.py deleted file mode 100644 index aa6b9ce..0000000 --- a/cli/koji_cli/commands/cmd_download_logs.py +++ /dev/null @@ -1,168 +0,0 @@ -from __future__ import absolute_import, division - -import os -import pprint -import sys -import traceback -from optparse import OptionParser - -import six -import six.moves.xmlrpc_client - -import koji - -from koji_cli.lib import ( - download_file, - ensure_connection, - error, - get_usage_str, - list_task_output_all_volumes, - warn -) - - -def anon_handle_download_logs(options, session, args): - "[download] Download logs for task" - - FAIL_LOG = "task_failed.log" - usage = "usage: %prog download-logs [options] [ ...]" - usage += "\n %prog download-logs [options] --nvr [ ...]" - usage += "\n" - usage += "\n" - usage += "Note this command only downloads task logs, not build logs." - usage += "\n" - parser = OptionParser(usage=get_usage_str(usage)) - parser.add_option("-r", "--recurse", action="store_true", - help="Process children of this task as well") - parser.add_option("--nvr", action="store_true", - help="Get the logs for the task associated with this build " - "Name-Version-Release.") - parser.add_option("-m", "--match", action="append", metavar="PATTERN", - help="Get only log filenames matching PATTERN (fnmatch). " - "May be used multiple times.") - parser.add_option("-c", "--continue", action="store_true", dest="cont", - help="Continue previous download") - parser.add_option("-d", "--dir", metavar="DIRECTORY", default='kojilogs', - help="Write logs to DIRECTORY") - (suboptions, args) = parser.parse_args(args) - - if len(args) < 1: - parser.error("Please specify at least one task id or n-v-r") - - def write_fail_log(task_log_dir, task_id): - """Gets output only from failed tasks""" - try: - result = session.getTaskResult(task_id) - # with current code, failed task results should always be faults, - # but that could change in the future - content = pprint.pformat(result) - except (six.moves.xmlrpc_client.Fault, koji.GenericError): - etype, e = sys.exc_info()[:2] - content = ''.join(traceback.format_exception_only(etype, e)) - full_filename = os.path.normpath(os.path.join(task_log_dir, FAIL_LOG)) - koji.ensuredir(os.path.dirname(full_filename)) - sys.stdout.write("Writing: %s\n" % full_filename) - with open(full_filename, 'wt') as fo: - fo.write(content) - - def download_log(task_log_dir, task_id, filename, blocksize=102400, volume=None): - # Create directories only if there is any log file to write to - # For each non-default volume create special sub-directory - if volume not in (None, 'DEFAULT'): - full_filename = os.path.normpath(os.path.join(task_log_dir, volume, filename)) - else: - full_filename = os.path.normpath(os.path.join(task_log_dir, filename)) - koji.ensuredir(os.path.dirname(full_filename)) - contents = 'IGNORE ME!' - if suboptions.cont and os.path.exists(full_filename): - sys.stdout.write("Continuing: %s\n" % full_filename) - fd = open(full_filename, 'ab') - offset = fd.tell() - else: - sys.stdout.write("Downloading: %s\n" % full_filename) - fd = open(full_filename, 'wb') - offset = 0 - try: - while contents: - contents = session.downloadTaskOutput(task_id, filename, offset=offset, - size=blocksize, volume=volume) - offset += len(contents) - if contents: - fd.write(contents) - finally: - fd.close() - - def save_logs(task_id, match, parent_dir='.', recurse=True): - assert task_id == int(task_id), "Task id must be number: %r" % task_id - task_info = session.getTaskInfo(task_id) - if task_info is None: - error("No such task: %d" % task_id) - files = list_task_output_all_volumes(session, task_id) - logs = [] # list of tuples (filename, volume) - for filename in files: - if not filename.endswith(".log"): - continue - if match and not koji.util.multi_fnmatch(filename, match): - continue - logs += [(filename, volume) for volume in files[filename]] - - task_log_dir = os.path.join(parent_dir, - "%s-%s" % (task_info["arch"], task_id)) - - count = 0 - state = koji.TASK_STATES[task_info['state']] - if state == 'FAILED': - if not match or koji.util.multi_fnmatch(FAIL_LOG, match): - write_fail_log(task_log_dir, task_id) - count += 1 - elif state not in ['CLOSED', 'CANCELED']: - warn("Task %s is %s\n" % (task_id, state)) - - for log_filename, log_volume in logs: - download_log(task_log_dir, task_id, log_filename, volume=log_volume) - count += 1 - - if count == 0 and not recurse: - warn("No logs found for task %i. Perhaps try --recurse?\n" % task_id) - - if recurse: - child_tasks = session.getTaskChildren(task_id) - for child_task in child_tasks: - save_logs(child_task['id'], match, task_log_dir, recurse) - - ensure_connection(session, options) - task_id = None - build_id = None - for arg in args: - if suboptions.nvr: - suboptions.recurse = True - binfo = session.getBuild(arg) - if binfo is None: - error("There is no build with n-v-r: %s" % arg) - if binfo.get('task_id'): - task_id = binfo['task_id'] - sys.stdout.write("Using task ID: %s\n" % task_id) - elif binfo.get('build_id'): - build_id = binfo['build_id'] - sys.stdout.write("Using build ID: %s\n" % build_id) - else: - try: - task_id = int(arg) - except ValueError: - error("Task id must be number: %r" % arg) - if task_id: - save_logs(task_id, suboptions.match, suboptions.dir, suboptions.recurse) - elif build_id: - logs = session.getBuildLogs(build_id) - match = suboptions.match - for log in logs: - url = os.path.join(options.topurl, log['path']) - filepath = os.path.join(os.getcwd(), '%s/%s/%s' % (suboptions.dir, - arg, log['name'])) - if not filepath.endswith(".log"): - continue - if match and not koji.util.multi_fnmatch(log['name'], match): - continue - download_file(url, filepath) - - diff --git a/cli/koji_cli/commands/cmd_download_task.py b/cli/koji_cli/commands/cmd_download_task.py deleted file mode 100644 index c8b4cb3..0000000 --- a/cli/koji_cli/commands/cmd_download_task.py +++ /dev/null @@ -1,122 +0,0 @@ -from __future__ import absolute_import, division - -import os -from optparse import OptionParser - -from six.moves import filter - -import koji - -from koji_cli.lib import ( - _running_in_bg, - download_file, - ensure_connection, - error, - get_usage_str, - list_task_output_all_volumes, - watch_tasks -) - - -def anon_handle_download_task(options, session, args): - "[download] Download the output of a build task" - usage = "usage: %prog download-task " - parser = OptionParser(usage=get_usage_str(usage)) - parser.add_option("--arch", dest="arches", metavar="ARCH", action="append", default=[], - help="Only download packages for this arch (may be used multiple times)") - parser.add_option("--logs", dest="logs", action="store_true", default=False, - help="Also download build logs") - parser.add_option("--topurl", metavar="URL", default=options.topurl, - help="URL under which Koji files are accessible") - parser.add_option("--noprogress", action="store_true", help="Do not display progress meter") - parser.add_option("--wait", action="store_true", - help="Wait for running tasks to finish, even if running in the background") - parser.add_option("--nowait", action="store_false", dest="wait", - help="Do not wait for running tasks to finish") - parser.add_option("-q", "--quiet", action="store_true", - help="Suppress output", default=options.quiet) - - (suboptions, args) = parser.parse_args(args) - if len(args) == 0: - parser.error("Please specify a task ID") - elif len(args) > 1: - parser.error("Only one task ID may be specified") - - base_task_id = int(args.pop()) - if len(suboptions.arches) > 0: - suboptions.arches = ",".join(suboptions.arches).split(",") - - ensure_connection(session, options) - - # get downloadable tasks - - base_task = session.getTaskInfo(base_task_id) - if not base_task: - error('No such task: %d' % base_task_id) - - if (suboptions.wait or (suboptions.wait is None and not _running_in_bg())) and \ - base_task['state'] not in ( - koji.TASK_STATES['CLOSED'], - koji.TASK_STATES['CANCELED'], - koji.TASK_STATES['FAILED']): - watch_tasks(session, [base_task_id], quiet=suboptions.quiet, - poll_interval=options.poll_interval, topurl=options.topurl) - - def check_downloadable(task): - return task["method"] == "buildArch" - - downloadable_tasks = [] - - if check_downloadable(base_task): - downloadable_tasks.append(base_task) - else: - subtasks = session.getTaskChildren(base_task_id) - downloadable_tasks.extend(list(filter(check_downloadable, subtasks))) - - # get files for download - downloads = [] - - for task in downloadable_tasks: - files = list_task_output_all_volumes(session, task["id"]) - for filename in files: - if filename.endswith(".rpm"): - for volume in files[filename]: - filearch = filename.split(".")[-2] - if len(suboptions.arches) == 0 or filearch in suboptions.arches: - downloads.append((task, filename, volume, filename)) - elif filename.endswith(".log") and suboptions.logs: - for volume in files[filename]: - # rename logs, they would conflict - new_filename = "%s.%s.log" % (filename.rstrip(".log"), task["arch"]) - downloads.append((task, filename, volume, new_filename)) - - if len(downloads) == 0: - error("No files for download found.") - - required_tasks = {} - for (task, nop, nop, nop) in downloads: - if task["id"] not in required_tasks: - required_tasks[task["id"]] = task - - for task_id in required_tasks: - if required_tasks[task_id]["state"] != koji.TASK_STATES.get("CLOSED"): - if task_id == base_task_id: - error("Task %d has not finished yet." % task_id) - else: - error("Child task %d has not finished yet." % task_id) - - # perform the download - number = 0 - pathinfo = koji.PathInfo(topdir=suboptions.topurl) - for (task, filename, volume, new_filename) in downloads: - number += 1 - if volume not in (None, 'DEFAULT'): - koji.ensuredir(volume) - new_filename = os.path.join(volume, new_filename) - if '..' in filename: - error('Invalid file name: %s' % filename) - url = '%s/%s/%s' % (pathinfo.work(volume), pathinfo.taskrelpath(task["id"]), filename) - download_file(url, new_filename, quiet=suboptions.quiet, noprogress=suboptions.noprogress, - size=len(downloads), num=number) - - diff --git a/cli/koji_cli/commands/cmd_edit_channel.py b/cli/koji_cli/commands/cmd_edit_channel.py deleted file mode 100644 index e0cd9a2..0000000 --- a/cli/koji_cli/commands/cmd_edit_channel.py +++ /dev/null @@ -1,46 +0,0 @@ -from __future__ import absolute_import, division - -from optparse import OptionParser - - -import koji - -from koji_cli.lib import ( - activate_session, - error, - get_usage_str -) - - -def handle_edit_channel(goptions, session, args): - "[admin] Edit a channel" - usage = "usage: %prog edit-channel [options] " - parser = OptionParser(usage=get_usage_str(usage)) - parser.add_option("--name", help="New channel name") - parser.add_option("--description", help="Description of channel") - parser.add_option("--comment", help="Comment of channel") - (options, args) = parser.parse_args(args) - if len(args) != 1: - parser.error("Incorrect number of arguments") - activate_session(session, goptions) - vals = {} - for key, val in options.__dict__.items(): - if val is not None: - vals[key] = val - cinfo = session.getChannel(args[0]) - if not cinfo: - error("No such channel: %s" % args[0]) - try: - result = session.editChannel(args[0], **vals) - except koji.GenericError as ex: - msg = str(ex) - if 'Invalid method:' in msg: - version = session.getKojiVersion() - error("editChannel is available on hub from Koji 1.26 version, your version is %s" % - version) - else: - print(msg) - if not result: - error("No changes made, please correct the command line") - - diff --git a/cli/koji_cli/commands/cmd_edit_external_repo.py b/cli/koji_cli/commands/cmd_edit_external_repo.py deleted file mode 100644 index 7824078..0000000 --- a/cli/koji_cli/commands/cmd_edit_external_repo.py +++ /dev/null @@ -1,62 +0,0 @@ -from __future__ import absolute_import, division - -from optparse import OptionParser - - -import koji - -from koji_cli.lib import ( - activate_session, - get_usage_str -) - - -def handle_edit_external_repo(goptions, session, args): - "[admin] Edit data for an external repo" - usage = "usage: %prog edit-external-repo [options] " - parser = OptionParser(usage=get_usage_str(usage)) - parser.add_option("--url", help="Change the url") - parser.add_option("--name", help="Change the name") - parser.add_option("-t", "--tag", metavar="TAG", help="Edit the repo properties for the tag.") - parser.add_option("-p", "--priority", metavar="PRIORITY", type='int', - help="Edit the priority of the repo for the tag specified by --tag.") - parser.add_option("-m", "--mode", metavar="MODE", - help="Edit the merge mode of the repo for the tag specified by --tag. " - "Options: %s." % ", ".join(koji.REPO_MERGE_MODES)) - parser.add_option("-a", "--arches", metavar="ARCH1,ARCH2, ...", - help="Use only subset of arches from given repo") - (options, args) = parser.parse_args(args) - if len(args) != 1: - parser.error("Incorrect number of arguments") - repo_opts = {} - if options.url: - repo_opts['url'] = options.url - if options.name: - repo_opts['name'] = options.name - tag_repo_opts = {} - if options.tag: - if options.priority is not None: - tag_repo_opts['priority'] = options.priority - if options.mode: - tag_repo_opts['merge_mode'] = options.mode - if options.arches is not None: - tag_repo_opts['arches'] = options.arches - if not tag_repo_opts: - parser.error("At least, one of priority and merge mode should be specified") - tag_repo_opts['tag_info'] = options.tag - tag_repo_opts['repo_info'] = args[0] - else: - for k in ('priority', 'mode', 'arches'): - if getattr(options, k) is not None: - parser.error("If %s is specified, --tag must be specified as well" % k) - - if not (repo_opts or tag_repo_opts): - parser.error("No changes specified") - - activate_session(session, goptions) - if repo_opts: - session.editExternalRepo(args[0], **repo_opts) - if tag_repo_opts: - session.editTagExternalRepo(**tag_repo_opts) - - diff --git a/cli/koji_cli/commands/cmd_edit_host.py b/cli/koji_cli/commands/cmd_edit_host.py deleted file mode 100644 index 00e9f04..0000000 --- a/cli/koji_cli/commands/cmd_edit_host.py +++ /dev/null @@ -1,60 +0,0 @@ -from __future__ import absolute_import, division - -from optparse import OptionParser - -from six.moves import zip - -import koji - -from koji_cli.lib import ( - activate_session, - error, - get_usage_str, - warn -) - - -def handle_edit_host(options, session, args): - "[admin] Edit a host" - usage = "usage: %prog edit-host [ ...] [options]" - parser = OptionParser(usage=get_usage_str(usage)) - parser.add_option("--arches", - help="Space or comma-separated list of supported architectures") - parser.add_option("--capacity", type="float", help="Capacity of this host") - parser.add_option("--description", metavar="DESC", help="Description of this host") - parser.add_option("--comment", help="A brief comment about this host") - (subopts, args) = parser.parse_args(args) - if not args: - parser.error("Please specify a hostname") - - activate_session(session, options) - - vals = {} - for key, val in subopts.__dict__.items(): - if val is not None: - vals[key] = val - if 'arches' in vals: - vals['arches'] = koji.parse_arches(vals['arches']) - - session.multicall = True - for host in args: - session.getHost(host) - error_hit = False - for host, [info] in zip(args, session.multiCall(strict=True)): - if not info: - warn("No such host: %s" % host) - error_hit = True - - if error_hit: - error("No changes made, please correct the command line") - - session.multicall = True - for host in args: - session.editHost(host, **vals) - for host, [result] in zip(args, session.multiCall(strict=True)): - if result: - print("Edited %s" % host) - else: - print("No changes made to %s" % host) - - diff --git a/cli/koji_cli/commands/cmd_edit_notification.py b/cli/koji_cli/commands/cmd_edit_notification.py deleted file mode 100644 index 76b3631..0000000 --- a/cli/koji_cli/commands/cmd_edit_notification.py +++ /dev/null @@ -1,67 +0,0 @@ -from __future__ import absolute_import, division - -from optparse import OptionParser - - -import koji - -from koji_cli.lib import ( - activate_session, - get_usage_str -) - - -def handle_edit_notification(goptions, session, args): - "[monitor] Edit user's notification" - usage = "usage: %prog edit-notification [options] " - parser = OptionParser(usage=get_usage_str(usage)) - parser.add_option("--package", help="Notifications for this package, '*' for all") - parser.add_option("--tag", help="Notifications for this tag, '*' for all") - parser.add_option("--success-only", action="store_true", default=None, - dest='success_only', help="Notify only on successful events") - parser.add_option("--no-success-only", action="store_false", - default=None, dest='success_only', help="Notify on all events") - (options, args) = parser.parse_args(args) - - if len(args) != 1: - parser.error("Only argument is notification ID") - - try: - n_id = int(args[0]) - except ValueError: - parser.error("Notification ID has to be numeric") - - if not options.package and not options.tag and options.success_only is None: - parser.error("Command need at least one option") - - activate_session(session, goptions) - - old = session.getBuildNotification(n_id) - - if options.package == '*': - package_id = None - elif options.package: - package_id = session.getPackageID(options.package) - if package_id is None: - parser.error("No such package: %s" % options.package) - else: - package_id = old['package_id'] - - if options.tag == '*': - tag_id = None - elif options.tag: - try: - tag_id = session.getTagID(options.tag, strict=True) - except koji.GenericError: - parser.error("No such tag: %s" % options.tag) - else: - tag_id = old['tag_id'] - - if options.success_only is not None: - success_only = options.success_only - else: - success_only = old['success_only'] - - session.updateNotification(n_id, package_id, tag_id, success_only) - - diff --git a/cli/koji_cli/commands/cmd_edit_permission.py b/cli/koji_cli/commands/cmd_edit_permission.py deleted file mode 100644 index f40fec6..0000000 --- a/cli/koji_cli/commands/cmd_edit_permission.py +++ /dev/null @@ -1,25 +0,0 @@ -from __future__ import absolute_import, division - -from optparse import OptionParser - - - -from koji_cli.lib import ( - activate_session, - get_usage_str -) - - -def handle_edit_permission(goptions, session, args): - "[admin] Edit a permission description" - usage = "usage: %prog edit-permission " - parser = OptionParser(usage=get_usage_str(usage)) - (options, args) = parser.parse_args(args) - if len(args) < 2: - parser.error("Please specify a permission and a description") - activate_session(session, goptions) - perm = args[0] - description = args[1] - session.editPermission(perm, description) - - diff --git a/cli/koji_cli/commands/cmd_edit_tag.py b/cli/koji_cli/commands/cmd_edit_tag.py deleted file mode 100644 index a3d7263..0000000 --- a/cli/koji_cli/commands/cmd_edit_tag.py +++ /dev/null @@ -1,78 +0,0 @@ -from __future__ import absolute_import, division - -from optparse import OptionParser - - -import koji - -from koji_cli.lib import ( - activate_session, - arg_filter, - get_usage_str -) - - -def handle_edit_tag(goptions, session, args): - "[admin] Alter tag information" - usage = "usage: %prog edit-tag [options] " - parser = OptionParser(usage=get_usage_str(usage)) - parser.add_option("--arches", help="Specify arches") - parser.add_option("--perm", help="Specify permission requirement") - parser.add_option("--no-perm", action="store_true", help="Remove permission requirement") - parser.add_option("--lock", action="store_true", help="Lock the tag") - parser.add_option("--unlock", action="store_true", help="Unlock the tag") - parser.add_option("--rename", help="Rename the tag") - parser.add_option("--maven-support", action="store_true", - help="Enable creation of Maven repos for this tag") - parser.add_option("--no-maven-support", action="store_true", - help="Disable creation of Maven repos for this tag") - parser.add_option("--include-all", action="store_true", - help="Include all packages in this tag when generating Maven repos") - parser.add_option("--no-include-all", action="store_true", - help="Do not include all packages in this tag when generating Maven repos") - parser.add_option("-x", "--extra", action="append", default=[], metavar="key=value", - help="Set tag extra option. JSON-encoded or simple value") - parser.add_option("-r", "--remove-extra", action="append", default=[], metavar="key", - help="Remove tag extra option") - parser.add_option("-b", "--block-extra", action="append", default=[], metavar="key", - help="Block inherited tag extra option") - (options, args) = parser.parse_args(args) - if len(args) != 1: - parser.error("Please specify a name for the tag") - activate_session(session, goptions) - tag = args[0] - opts = {} - if options.arches: - opts['arches'] = koji.parse_arches(options.arches) - if options.no_perm: - opts['perm'] = None - elif options.perm: - opts['perm'] = options.perm - if options.unlock: - opts['locked'] = False - if options.lock: - opts['locked'] = True - if options.rename: - opts['name'] = options.rename - if options.maven_support: - opts['maven_support'] = True - if options.no_maven_support: - opts['maven_support'] = False - if options.include_all: - opts['maven_include_all'] = True - if options.no_include_all: - opts['maven_include_all'] = False - if options.extra: - extra = {} - for xopt in options.extra: - key, value = xopt.split('=', 1) - if key in extra: - parser.error("Duplicate extra key: %s" % key) - extra[key] = arg_filter(value, parse_json=True) - opts['extra'] = extra - opts['remove_extra'] = options.remove_extra - opts['block_extra'] = options.block_extra - # XXX change callname - session.editTag2(tag, **opts) - - diff --git a/cli/koji_cli/commands/cmd_edit_tag_inheritance.py b/cli/koji_cli/commands/cmd_edit_tag_inheritance.py deleted file mode 100644 index 3a19bae..0000000 --- a/cli/koji_cli/commands/cmd_edit_tag_inheritance.py +++ /dev/null @@ -1,94 +0,0 @@ -from __future__ import absolute_import, division - -from optparse import OptionParser - - - -from koji_cli.lib import ( - activate_session, - error, - get_usage_str -) - - -def handle_edit_tag_inheritance(goptions, session, args): - """[admin] Edit tag inheritance""" - usage = "usage: %prog edit-tag-inheritance [options] " - parser = OptionParser(usage=get_usage_str(usage)) - parser.add_option("--priority", help="Specify a new priority") - parser.add_option("--maxdepth", help="Specify max depth") - parser.add_option("--intransitive", action="store_true", help="Set intransitive") - parser.add_option("--noconfig", action="store_true", help="Set to packages only") - parser.add_option("--pkg-filter", help="Specify the package filter") - (options, args) = parser.parse_args(args) - - if len(args) < 1: - parser.error("This command takes at least one argument: a tag name or ID") - - if len(args) > 3: - parser.error("This command takes at most three argument: a tag name or ID, " - "a parent tag name or ID, and a priority") - - activate_session(session, goptions) - - tag = session.getTag(args[0]) - if not tag: - parser.error("No such tag: %s" % args[0]) - - parent = None - priority = None - if len(args) > 1: - parent = session.getTag(args[1]) - if not parent: - parser.error("No such tag: %s" % args[1]) - if len(args) > 2: - priority = args[2] - - data = session.getInheritanceData(tag['id']) - if parent and data: - data = [datum for datum in data if datum['parent_id'] == parent['id']] - if priority and data: - data = [datum for datum in data if datum['priority'] == priority] - - if len(data) == 0: - error("No inheritance link found to remove. Please check your arguments") - elif len(data) > 1: - print("Multiple matches for tag.") - if not parent: - error("Please specify a parent on the command line.") - if not priority: - error("Please specify a priority on the command line.") - error("Error: Key constraints may be broken. Exiting.") - - # len(data) == 1 - data = data[0] - - inheritanceData = session.getInheritanceData(tag['id']) - samePriority = [datum for datum in inheritanceData if datum['priority'] == options.priority] - if samePriority: - error("Error: There is already an active inheritance with that priority on %s, " - "please specify a different priority with --priority." % tag['name']) - - new_data = data.copy() - if options.priority is not None and options.priority.isdigit(): - new_data['priority'] = int(options.priority) - if options.maxdepth is not None: - if options.maxdepth.isdigit(): - new_data['maxdepth'] = int(options.maxdepth) - elif options.maxdepth.lower() == "none": - new_data['maxdepth'] = None - else: - error("Invalid maxdepth: %s" % options.maxdepth) - if options.intransitive: - new_data['intransitive'] = options.intransitive - if options.noconfig: - new_data['noconfig'] = options.noconfig - if options.pkg_filter: - new_data['pkg_filter'] = options.pkg_filter - - # find the data we want to edit and replace it - index = inheritanceData.index(data) - inheritanceData[index] = new_data - session.setInheritanceData(tag['id'], inheritanceData) - - diff --git a/cli/koji_cli/commands/cmd_edit_target.py b/cli/koji_cli/commands/cmd_edit_target.py deleted file mode 100644 index 089dcd4..0000000 --- a/cli/koji_cli/commands/cmd_edit_target.py +++ /dev/null @@ -1,56 +0,0 @@ -from __future__ import absolute_import, division - -from optparse import OptionParser - - -import koji - -from koji_cli.lib import ( - activate_session, - error, - get_usage_str -) - - -def handle_edit_target(goptions, session, args): - "[admin] Set the name, build_tag, and/or dest_tag of an existing build target to new values" - usage = "usage: %prog edit-target [options] " - parser = OptionParser(usage=get_usage_str(usage)) - parser.add_option("--rename", help="Specify new name for target") - parser.add_option("--build-tag", help="Specify a different build tag") - parser.add_option("--dest-tag", help="Specify a different destination tag") - - (options, args) = parser.parse_args(args) - - if len(args) != 1: - parser.error("Please specify a build target") - activate_session(session, goptions) - - if not (session.hasPerm('admin') or session.hasPerm('target')): - parser.error("This action requires target or admin privileges") - - targetInfo = session.getBuildTarget(args[0]) - if targetInfo is None: - raise koji.GenericError("No such build target: %s" % args[0]) - - targetInfo['orig_name'] = targetInfo['name'] - - if options.rename: - targetInfo['name'] = options.rename - if options.build_tag: - targetInfo['build_tag_name'] = options.build_tag - chkbuildtag = session.getTag(options.build_tag) - if not chkbuildtag: - error("No such tag: %s" % options.build_tag) - if not chkbuildtag.get("arches", None): - error("Build tag has no arches: %s" % options.build_tag) - if options.dest_tag: - chkdesttag = session.getTag(options.dest_tag) - if not chkdesttag: - error("No such destination tag: %s" % options.dest_tag) - targetInfo['dest_tag_name'] = options.dest_tag - - session.editBuildTarget(targetInfo['orig_name'], targetInfo['name'], - targetInfo['build_tag_name'], targetInfo['dest_tag_name']) - - diff --git a/cli/koji_cli/commands/cmd_edit_user.py b/cli/koji_cli/commands/cmd_edit_user.py deleted file mode 100644 index 6bdd166..0000000 --- a/cli/koji_cli/commands/cmd_edit_user.py +++ /dev/null @@ -1,42 +0,0 @@ -from __future__ import absolute_import, division - -from optparse import OptionParser - - - -from koji_cli.lib import ( - activate_session, - arg_filter, - get_usage_str -) - - -def handle_edit_user(goptions, session, args): - "[admin] Alter user information" - usage = "usage: %prog edit-user [options]" - parser = OptionParser(usage=get_usage_str(usage)) - parser.add_option("--rename", help="Rename the user") - parser.add_option("--edit-krb", action="append", default=[], metavar="OLD=NEW", - help="Change kerberos principal of the user") - parser.add_option("--add-krb", action="append", default=[], metavar="KRB", - help="Add kerberos principal of the user") - parser.add_option("--remove-krb", action="append", default=[], metavar="KRB", - help="Remove kerberos principal of the user") - (options, args) = parser.parse_args(args) - if len(args) < 1: - parser.error("You must specify the username of the user to edit") - elif len(args) > 1: - parser.error("This command only accepts one argument (username)") - activate_session(session, goptions) - user = args[0] - princ_mappings = [] - for p in options.edit_krb: - old, new = p.split('=', 1) - princ_mappings.append({'old': arg_filter(old), 'new': arg_filter(new)}) - for a in options.add_krb: - princ_mappings.append({'old': None, 'new': arg_filter(a)}) - for r in options.remove_krb: - princ_mappings.append({'old': arg_filter(r), 'new': None}) - session.editUser(user, options.rename, princ_mappings) - - diff --git a/cli/koji_cli/commands/cmd_enable_channel.py b/cli/koji_cli/commands/cmd_enable_channel.py deleted file mode 100644 index ba5825c..0000000 --- a/cli/koji_cli/commands/cmd_enable_channel.py +++ /dev/null @@ -1,39 +0,0 @@ -from __future__ import absolute_import, division - -from optparse import OptionParser - -from six.moves import zip - - -from koji_cli.lib import ( - activate_session, - error, - get_usage_str -) - - -def handle_enable_channel(goptions, session, args): - "[admin] Mark one or more channels as enabled" - usage = "usage: %prog enable-channel [options] [ ...]" - parser = OptionParser(usage=get_usage_str(usage)) - parser.add_option("--comment", help="Comment indicating why the channel(s) are being enabled") - (options, args) = parser.parse_args(args) - - if not args: - parser.error("At least one channel must be specified") - - activate_session(session, goptions) - with session.multicall() as m: - result = [m.getChannel(channel, strict=False) for channel in args] - error_hit = False - for channel, id in zip(args, result): - if not id.result: - print("No such channel: %s" % channel) - error_hit = True - if error_hit: - error("No changes made. Please correct the command line.") - - with session.multicall() as m: - [m.enableChannel(channel, comment=options.comment) for channel in args] - - diff --git a/cli/koji_cli/commands/cmd_enable_host.py b/cli/koji_cli/commands/cmd_enable_host.py deleted file mode 100644 index b562364..0000000 --- a/cli/koji_cli/commands/cmd_enable_host.py +++ /dev/null @@ -1,43 +0,0 @@ -from __future__ import absolute_import, division - -from optparse import OptionParser - -from six.moves import zip - - -from koji_cli.lib import ( - activate_session, - error, - get_usage_str -) - - -def handle_enable_host(goptions, session, args): - "[admin] Mark one or more hosts as enabled" - usage = "usage: %prog enable-host [options] [ ...]" - parser = OptionParser(usage=get_usage_str(usage)) - parser.add_option("--comment", help="Comment indicating why the host(s) are being enabled") - (options, args) = parser.parse_args(args) - - if not args: - parser.error("At least one host must be specified") - - activate_session(session, goptions) - session.multicall = True - for host in args: - session.getHost(host) - error_hit = False - for host, [id] in zip(args, session.multiCall(strict=True)): - if not id: - print("No such host: %s" % host) - error_hit = True - if error_hit: - error("No changes made. Please correct the command line.") - session.multicall = True - for host in args: - session.enableHost(host) - if options.comment is not None: - session.editHost(host, comment=options.comment) - session.multiCall(strict=True) - - diff --git a/cli/koji_cli/commands/cmd_enable_user.py b/cli/koji_cli/commands/cmd_enable_user.py deleted file mode 100644 index 2e18d3e..0000000 --- a/cli/koji_cli/commands/cmd_enable_user.py +++ /dev/null @@ -1,26 +0,0 @@ -from __future__ import absolute_import, division - -from optparse import OptionParser - - - -from koji_cli.lib import ( - activate_session, - get_usage_str -) - - -def handle_enable_user(goptions, session, args): - "[admin] Enable logins by a user" - usage = "usage: %prog enable-user " - parser = OptionParser(usage=get_usage_str(usage)) - (options, args) = parser.parse_args(args) - if len(args) < 1: - parser.error("You must specify the username of the user to enable") - elif len(args) > 1: - parser.error("This command only accepts one argument (username)") - username = args[0] - activate_session(session, goptions) - session.enableUser(username) - - diff --git a/cli/koji_cli/commands/cmd_free_task.py b/cli/koji_cli/commands/cmd_free_task.py deleted file mode 100644 index 9fc009d..0000000 --- a/cli/koji_cli/commands/cmd_free_task.py +++ /dev/null @@ -1,30 +0,0 @@ -from __future__ import absolute_import, division - -from optparse import OptionParser - - - -from koji_cli.lib import ( - activate_session, - get_usage_str -) - - -def handle_free_task(goptions, session, args): - "[admin] Free a task" - usage = "usage: %prog free-task [options] [ ...]" - parser = OptionParser(usage=get_usage_str(usage)) - (options, args) = parser.parse_args(args) - activate_session(session, goptions) - tlist = [] - for task_id in args: - try: - tlist.append(int(task_id)) - except ValueError: - parser.error("task_id must be an integer") - if not tlist: - parser.error("please specify at least one task_id") - for task_id in tlist: - session.freeTask(task_id) - - diff --git a/cli/koji_cli/commands/cmd_grant_cg_access.py b/cli/koji_cli/commands/cmd_grant_cg_access.py deleted file mode 100644 index 366fe6b..0000000 --- a/cli/koji_cli/commands/cmd_grant_cg_access.py +++ /dev/null @@ -1,32 +0,0 @@ -from __future__ import absolute_import, division - -from optparse import OptionParser - - - -from koji_cli.lib import ( - activate_session, - get_usage_str -) - - -def handle_grant_cg_access(goptions, session, args): - "[admin] Add a user to a content generator" - usage = "usage: %prog grant-cg-access " - parser = OptionParser(usage=get_usage_str(usage)) - parser.add_option("--new", action="store_true", help="Create a new content generator") - (options, args) = parser.parse_args(args) - if len(args) != 2: - parser.error("Please specify a user and content generator") - activate_session(session, goptions) - user = args[0] - cg = args[1] - uinfo = session.getUser(user) - if uinfo is None: - parser.error("No such user: %s" % user) - kwargs = {} - if options.new: - kwargs['create'] = True - session.grantCGAccess(uinfo['name'], cg, **kwargs) - - diff --git a/cli/koji_cli/commands/cmd_grant_permission.py b/cli/koji_cli/commands/cmd_grant_permission.py deleted file mode 100644 index c72ee6c..0000000 --- a/cli/koji_cli/commands/cmd_grant_permission.py +++ /dev/null @@ -1,43 +0,0 @@ -from __future__ import absolute_import, division - -from optparse import OptionParser - - - -from koji_cli.lib import ( - activate_session, - get_usage_str -) - - -def handle_grant_permission(goptions, session, args): - "[admin] Grant a permission to a user" - usage = "usage: %prog grant-permission [options] [ ...]" - parser = OptionParser(usage=get_usage_str(usage)) - parser.add_option("--new", action="store_true", - help="Create this permission if the permission does not exist") - parser.add_option("--description", - help="Add description about new permission") - (options, args) = parser.parse_args(args) - if len(args) < 2: - parser.error("Please specify a permission and at least one user") - activate_session(session, goptions) - perm = args[0] - names = args[1:] - users = [] - for n in names: - user = session.getUser(n) - if user is None: - parser.error("No such user: %s" % n) - users.append(user) - kwargs = {} - if options.new: - kwargs['create'] = True - if options.description: - kwargs['description'] = options.description - if options.description and not options.new: - parser.error("Option new must be specified with option description.") - for user in users: - session.grantPermission(user['name'], perm, **kwargs) - - diff --git a/cli/koji_cli/commands/cmd_hostinfo.py b/cli/koji_cli/commands/cmd_hostinfo.py deleted file mode 100644 index eb0fd7c..0000000 --- a/cli/koji_cli/commands/cmd_hostinfo.py +++ /dev/null @@ -1,93 +0,0 @@ -from __future__ import absolute_import, division - -from optparse import OptionParser - -from six.moves import range - -import koji - -from koji_cli.lib import ( - ensure_connection, - error, - get_usage_str, - warn -) - - -def anon_handle_hostinfo(goptions, session, args): - "[info] Print basic information about a host" - usage = "usage: %prog hostinfo [options] [ ...]" - parser = OptionParser(usage=get_usage_str(usage)) - (options, args) = parser.parse_args(args) - if len(args) < 1: - parser.error("Please specify a host") - ensure_connection(session, goptions) - error_hit = False - for host in args: - if host.isdigit(): - host = int(host) - info = session.getHost(host) - if info is None: - warn("No such host: %s\n" % host) - error_hit = True - continue - print("Name: %(name)s" % info) - print("ID: %(id)d" % info) - print("Arches: %(arches)s" % info) - print("Capacity: %(capacity)s" % info) - print("Task Load: %(task_load).2f" % info) - if info['description']: - description = info['description'].splitlines() - print("Description: %s" % description[0]) - for line in description[1:]: - print("%s%s" % (" " * 13, line)) - else: - print("Description:") - if info['comment']: - comment = info['comment'].splitlines() - print("Comment: %s" % comment[0]) - for line in comment[1:]: - print("%s%s" % (" " * 9, line)) - else: - print("Comment:") - print("Enabled: %s" % (info['enabled'] and 'yes' or 'no')) - print("Ready: %s" % (info['ready'] and 'yes' or 'no')) - try: - update = session.getLastHostUpdate(info['id'], ts=True) - except koji.ParameterError: - # Hubs prior to v1.25.0 do not have a "ts" parameter for getLastHostUpdate - update = session.getLastHostUpdate(info['id']) - if update is None: - update = "never" - else: - update = koji.formatTimeLong(update) - print("Last Update: %s" % update) - print("Channels: %s" % ' '.join([c['name'] - for c in session.listChannels(hostID=info['id'])])) - print("Active Buildroots:") - states = {0: "INIT", 1: "WAITING", 2: "BUILDING"} - rows = [('NAME', 'STATE', 'CREATION TIME')] - for s in range(0, 3): - for b in session.listBuildroots(hostID=info['id'], state=s): - rows.append((("%s-%s-%s" % (b['tag_name'], b['id'], b['repo_id'])), states[s], - b['create_event_time'][:b['create_event_time'].find('.')])) - if len(rows) > 1: - for row in rows: - print("%-50s %-10s %-20s" % row) - else: - print("None") - if error_hit: - error() - - -def _multicall_with_check(session, batch_size): - """Helper for running multicall inside handle_clone_tag""" - err = False - for r in session.multiCall(batch=batch_size): - if isinstance(r, dict): - warn(r['faultString']) - err = True - if err: - error('Errors during the last call. Target tag could be inconsistent.') - - diff --git a/cli/koji_cli/commands/cmd_image_build.py b/cli/koji_cli/commands/cmd_image_build.py deleted file mode 100644 index 1101b7c..0000000 --- a/cli/koji_cli/commands/cmd_image_build.py +++ /dev/null @@ -1,277 +0,0 @@ -from __future__ import absolute_import, division - -import os -from optparse import OptionParser - - -import koji - -from koji_cli.lib import ( - _progress_callback, - _running_in_bg, - activate_session, - get_usage_str, - unique_path, - watch_tasks -) - - -def handle_image_build(options, session, args): - """[build] Create a disk image given an install tree""" - formats = ('vmdk', 'qcow', 'qcow2', 'vdi', 'vpc', 'rhevm-ova', - 'vsphere-ova', 'vagrant-virtualbox', 'vagrant-libvirt', - 'vagrant-vmware-fusion', 'vagrant-hyperv', 'docker', 'raw-xz', - 'liveimg-squashfs', 'tar-gz') - usage = "usage: %prog image-build [options] " \ - " [ ...]" - usage += "\n %prog image-build --config \n" - parser = OptionParser(usage=get_usage_str(usage)) - parser.add_option("--background", action="store_true", - help="Run the image creation task at a lower priority") - parser.add_option("--config", - help="Use a configuration file to define image-build options " - "instead of command line options (they will be ignored).") - parser.add_option("--disk-size", default=10, help="Set the disk device size in gigabytes") - parser.add_option("--distro", - help="specify the RPM based distribution the image will be based " - "on with the format RHEL-X.Y, CentOS-X.Y, SL-X.Y, or Fedora-NN. " - "The packages for the Distro you choose must have been built " - "in this system.") - parser.add_option("--format", default=[], action="append", - help="Convert results to one or more formats " - "(%s), this option may be used " - "multiple times. By default, specifying this option will " - "omit the raw disk image (which is 10G in size) from the " - "build results. If you really want it included with converted " - "images, pass in 'raw' as an option." % ', '.join(formats)) - parser.add_option("--kickstart", help="Path to a local kickstart file") - parser.add_option("--ksurl", metavar="SCMURL", - help="The URL to the SCM containing the kickstart file") - parser.add_option("--ksversion", metavar="VERSION", - help="The syntax version used in the kickstart file") - parser.add_option("--noprogress", action="store_true", - help="Do not display progress of the upload") - parser.add_option("--noverifyssl", action="store_true", - help="Use the noverifyssl option for the install tree and all repos. " - "This option is only allowed if enabled on the builder.") - parser.add_option("--nowait", action="store_false", dest="wait", - help="Don't wait on image creation") - parser.add_option("--ova-option", action="append", - help="Override a value in the OVA description XML. Provide a value " - "in a name=value format, such as 'ovf_memory_mb=6144'") - parser.add_option("--factory-parameter", nargs=2, action="append", - help="Pass a parameter to Image Factory. The results are highly specific " - "to the image format being created. This is a two argument parameter " - "that can be specified an arbitrary number of times. For example: " - "--factory-parameter docker_cmd '[ \"/bin/echo Hello World\" ]'") - parser.add_option("--release", help="Forcibly set the release field") - parser.add_option("--repo", action="append", - help="Specify a repo that will override the repo used to install " - "RPMs in the image. May be used multiple times. The " - "build tag repo associated with the target is the default.") - parser.add_option("--scratch", action="store_true", help="Create a scratch image") - parser.add_option("--skip-tag", action="store_true", help="Do not attempt to tag package") - parser.add_option("--can-fail", action="store", dest="optional_arches", - metavar="ARCH1,ARCH2,...", default="", - help="List of archs which are not blocking for build (separated by commas.") - parser.add_option("--specfile", metavar="URL", - help="SCM URL to spec file fragment to use to generate wrapper RPMs") - parser.add_option("--wait", action="store_true", - help="Wait on the image creation, even if running in the background") - - (task_options, args) = parser.parse_args(args) - - if task_options.config: - section = 'image-build' - config = koji.read_config_files([(task_options.config, True)]) - if not config.has_section(section): - parser.error("single section called [%s] is required" % section) - # pluck out the positional arguments first - args = [] - for arg in ('name', 'version', 'target', 'install_tree'): - args.append(config.get(section, arg)) - config.remove_option(section, arg) - args.extend(config.get(section, 'arches').split(',')) - config.remove_option(section, 'arches') - # turn comma-separated options into lists - for arg in ('repo', 'format'): - if config.has_option(section, arg): - setattr(task_options, arg, config.get(section, arg).split(',')) - config.remove_option(section, arg) - if config.has_option(section, 'can_fail'): - setattr(task_options, 'optional_arches', config.get(section, 'can_fail').split(',')) - config.remove_option(section, 'can_fail') - # handle everything else - for k, v in config.items(section): - setattr(task_options, k, v) - - # ova-options belong in their own section - section = 'ova-options' - if config.has_section(section): - task_options.ova_option = [] - for k, v in config.items(section): - task_options.ova_option.append('%s=%s' % (k, v)) - - # as do factory-parameters - section = 'factory-parameters' - if config.has_section(section): - task_options.factory_parameter = [] - for k, v in config.items(section): - # We do this, rather than a dict, to match what argparse spits out - task_options.factory_parameter.append((k, v)) - - else: - if len(args) < 5: - parser.error("At least five arguments are required: a name, a version, " - "a build target, a URL to an install tree, and 1 or more architectures.") - if not task_options.ksurl and not task_options.kickstart: - parser.error('You must specify --kickstart') - if not task_options.distro: - parser.error( - "You must specify --distro. Examples: Fedora-16, RHEL-6.4, SL-6.4 or CentOS-6.4") - return _build_image_oz(options, task_options, session, args) - - -def _build_image(options, task_opts, session, args, img_type): - """ - A private helper function that houses common CLI code for building - images with chroot-based tools. - """ - - if img_type not in ('livecd', 'appliance', 'livemedia'): - raise koji.GenericError('Unrecognized image type: %s' % img_type) - activate_session(session, options) - - # Set the task's priority. Users can only lower it with --background. - priority = None - if task_opts.background: - # relative to koji.PRIO_DEFAULT; higher means a "lower" priority. - priority = 5 - if _running_in_bg() or task_opts.noprogress: - callback = None - else: - callback = _progress_callback - - # We do some early sanity checking of the given target. - # Kojid gets these values again later on, but we check now as a convenience - # for the user. - target = args[2] - tmp_target = session.getBuildTarget(target) - if not tmp_target: - raise koji.GenericError("No such build target: %s" % target) - dest_tag = session.getTag(tmp_target['dest_tag']) - if not dest_tag: - raise koji.GenericError("No such destination tag: %s" % tmp_target['dest_tag_name']) - - # Set the architecture - if img_type == 'livemedia': - # livemedia accepts multiple arches - arch = [koji.canonArch(a) for a in args[3].split(",")] - else: - arch = koji.canonArch(args[3]) - - # Upload the KS file to the staging area. - # If it's a URL, it's kojid's job to go get it when it does the checkout. - ksfile = args[4] - - if not task_opts.ksurl: - serverdir = unique_path('cli-' + img_type) - session.uploadWrapper(ksfile, serverdir, callback=callback) - ksfile = os.path.join(serverdir, os.path.basename(ksfile)) - print('') - - hub_opts = {} - passthru_opts = [ - 'format', 'install_tree_url', 'isoname', 'ksurl', - 'ksversion', 'release', 'repo', 'scratch', 'skip_tag', - 'specfile', 'vcpu', 'vmem', 'volid', 'optional_arches', - 'lorax_dir', 'lorax_url', 'nomacboot', 'ksrepo', - 'squashfs_only', 'compress_arg', - ] - for opt in passthru_opts: - val = getattr(task_opts, opt, None) - if val is not None: - hub_opts[opt] = val - - if 'optional_arches' in hub_opts: - hub_opts['optional_arches'] = hub_opts['optional_arches'].split(',') - # finally, create the task. - task_id = session.buildImage(args[0], args[1], arch, target, ksfile, - img_type, opts=hub_opts, priority=priority) - - if not options.quiet: - print("Created task: %d" % task_id) - print("Task info: %s/taskinfo?taskID=%s" % (options.weburl, task_id)) - if task_opts.wait or (task_opts.wait is None and not _running_in_bg()): - session.logout() - return watch_tasks(session, [task_id], quiet=options.quiet, - poll_interval=options.poll_interval, topurl=options.topurl) - - -def _build_image_oz(options, task_opts, session, args): - """ - A private helper function that houses common CLI code for building - images with Oz and ImageFactory - """ - activate_session(session, options) - - # Set the task's priority. Users can only lower it with --background. - priority = None - if task_opts.background: - # relative to koji.PRIO_DEFAULT; higher means a "lower" priority. - priority = 5 - if _running_in_bg() or task_opts.noprogress: - callback = None - else: - callback = _progress_callback - - # We do some early sanity checking of the given target. - # Kojid gets these values again later on, but we check now as a convenience - # for the user. - target = args[2] - tmp_target = session.getBuildTarget(target) - if not tmp_target: - raise koji.GenericError("No such build target: %s" % target) - dest_tag = session.getTag(tmp_target['dest_tag']) - if not dest_tag: - raise koji.GenericError("No such destination tag: %s" % tmp_target['dest_tag_name']) - - # Set the architectures - arches = [] - for arch in args[4:]: - arches.append(koji.canonArch(arch)) - - # Upload the KS file to the staging area. - # If it's a URL, it's kojid's job to go get it when it does the checkout. - if not task_opts.ksurl: - if not task_opts.scratch: - # only scratch builds can omit ksurl - raise koji.GenericError("Non-scratch builds must provide ksurl") - ksfile = task_opts.kickstart - serverdir = unique_path('cli-image') - session.uploadWrapper(ksfile, serverdir, callback=callback) - task_opts.kickstart = os.path.join('work', serverdir, - os.path.basename(ksfile)) - print('') - - hub_opts = {} - for opt in ('ksurl', 'ksversion', 'kickstart', 'scratch', 'repo', - 'release', 'skip_tag', 'specfile', 'distro', 'format', - 'disk_size', 'ova_option', 'factory_parameter', - 'optional_arches', 'noverifyssl'): - val = getattr(task_opts, opt, None) - if val is not None: - hub_opts[opt] = val - # finally, create the task. - task_id = session.buildImageOz(args[0], args[1], arches, target, args[3], - opts=hub_opts, priority=priority) - - if not options.quiet: - print("Created task: %d" % task_id) - print("Task info: %s/taskinfo?taskID=%s" % (options.weburl, task_id)) - if task_opts.wait or (task_opts.wait is None and not _running_in_bg()): - session.logout() - return watch_tasks(session, [task_id], quiet=options.quiet, - poll_interval=options.poll_interval, topurl=options.topurl) - - diff --git a/cli/koji_cli/commands/cmd_image_build_indirection.py b/cli/koji_cli/commands/cmd_image_build_indirection.py deleted file mode 100644 index 1439386..0000000 --- a/cli/koji_cli/commands/cmd_image_build_indirection.py +++ /dev/null @@ -1,151 +0,0 @@ -from __future__ import absolute_import, division - -import os -from optparse import OptionParser - - -import koji - -from koji_cli.lib import ( - _progress_callback, - _running_in_bg, - activate_session, - get_usage_str, - unique_path, - watch_tasks -) - - -def handle_image_build_indirection(options, session, args): - """[build] Create a disk image using other disk images via the Indirection plugin""" - usage = "usage: %prog image-build-indirection [base_image] [utility_image] " \ - "[indirection_build_template]" - usage += "\n %prog image-build --config \n" - parser = OptionParser(usage=get_usage_str(usage)) - parser.add_option("--config", - help="Use a configuration file to define image-build options " - "instead of command line options (they will be ignored).") - parser.add_option("--background", action="store_true", - help="Run the image creation task at a lower priority") - parser.add_option("--name", help="Name of the output image") - parser.add_option("--version", help="Version of the output image") - parser.add_option("--release", help="Release of the output image") - parser.add_option("--arch", help="Architecture of the output image and input images") - parser.add_option("--target", help="Build target to use for the indirection build") - parser.add_option("--skip-tag", action="store_true", help="Do not tag the resulting build") - parser.add_option("--base-image-task", - help="ID of the createImage task of the base image to be used") - parser.add_option("--base-image-build", help="NVR or build ID of the base image to be used") - parser.add_option("--utility-image-task", - help="ID of the createImage task of the utility image to be used") - parser.add_option("--utility-image-build", - help="NVR or build ID of the utility image to be used") - parser.add_option("--indirection-template", - help="Name of the local file, or SCM file containing the template used to " - "drive the indirection plugin") - parser.add_option("--indirection-template-url", - help="SCM URL containing the template used to drive the indirection plugin") - parser.add_option("--results-loc", - help="Relative path inside the working space image where the results " - "should be extracted from") - parser.add_option("--scratch", action="store_true", help="Create a scratch image") - parser.add_option("--wait", action="store_true", - help="Wait on the image creation, even if running in the background") - parser.add_option("--nowait", action="store_false", dest="wait", - help="Do not wait on the image creation") - parser.add_option("--noprogress", action="store_true", - help="Do not display progress of the upload") - - (task_options, args) = parser.parse_args(args) - _build_image_indirection(options, task_options, session, args) - - -def _build_image_indirection(options, task_opts, session, args): - """ - A private helper function for builds using the indirection plugin of ImageFactory - """ - - # Do some sanity checks before even attempting to create the session - if not (bool(task_opts.utility_image_task) != - bool(task_opts.utility_image_build)): - raise koji.GenericError("You must specify either a utility-image task or build ID/NVR") - - if not (bool(task_opts.base_image_task) != - bool(task_opts.base_image_build)): - raise koji.GenericError("You must specify either a base-image task or build ID/NVR") - - required_opts = ['name', 'version', 'arch', 'target', 'indirection_template', 'results_loc'] - optional_opts = ['indirection_template_url', 'scratch', 'utility_image_task', - 'utility_image_build', 'base_image_task', 'base_image_build', 'release', - 'skip_tag'] - - missing = [] - for opt in required_opts: - if not getattr(task_opts, opt, None): - missing.append(opt) - - if len(missing) > 0: - print("Missing the following required options: %s" % - ' '.join(['--%s' % o.replace('_', '-') for o in missing])) - raise koji.GenericError("Missing required options specified above") - - activate_session(session, options) - - # Set the task's priority. Users can only lower it with --background. - priority = None - if task_opts.background: - # relative to koji.PRIO_DEFAULT; higher means a "lower" priority. - priority = 5 - if _running_in_bg() or task_opts.noprogress: - callback = None - else: - callback = _progress_callback - - # We do some early sanity checking of the given target. - # Kojid gets these values again later on, but we check now as a convenience - # for the user. - - tmp_target = session.getBuildTarget(task_opts.target) - if not tmp_target: - raise koji.GenericError("No such build target: %s" % tmp_target) - dest_tag = session.getTag(tmp_target['dest_tag']) - if not dest_tag: - raise koji.GenericError("No such destination tag: %s" % tmp_target['dest_tag_name']) - - # Set the architecture - task_opts.arch = koji.canonArch(task_opts.arch) - - # Upload the indirection template file to the staging area. - # If it's a URL, it's kojid's job to go get it when it does the checkout. - if not task_opts.indirection_template_url: - if not task_opts.scratch: - # only scratch builds can omit indirection_template_url - raise koji.GenericError( - "Non-scratch builds must provide a URL for the indirection template") - templatefile = task_opts.indirection_template - serverdir = unique_path('cli-image-indirection') - session.uploadWrapper(templatefile, serverdir, callback=callback) - task_opts.indirection_template = os.path.join('work', serverdir, - os.path.basename(templatefile)) - print('') - - hub_opts = {} - # Just pass everything in as opts. No posiitonal arguments at all. Why not? - for opt in required_opts + optional_opts: - val = getattr(task_opts, opt, None) - # We pass these through even if they are None - # The builder code can then check if they are set without using getattr - hub_opts[opt] = val - - # finally, create the task. - task_id = session.buildImageIndirection(opts=hub_opts, - priority=priority) - - if not options.quiet: - print("Created task: %d" % task_id) - print("Task info: %s/taskinfo?taskID=%s" % (options.weburl, task_id)) - if task_opts.wait or (task_opts.wait is None and not _running_in_bg()): - session.logout() - return watch_tasks(session, [task_id], quiet=options.quiet, topurl=options.topurl) - - diff --git a/cli/koji_cli/commands/cmd_import.py b/cli/koji_cli/commands/cmd_import.py deleted file mode 100644 index cd46130..0000000 --- a/cli/koji_cli/commands/cmd_import.py +++ /dev/null @@ -1,166 +0,0 @@ -from __future__ import absolute_import, division - -import os -import sys -from optparse import OptionParser - - -import koji -from koji.util import to_list - -from koji_cli.lib import ( - activate_session, - get_usage_str, - linked_upload, - unique_path, - warn -) - - -def handle_import(goptions, session, args): - "[admin] Import externally built RPMs into the database" - usage = "usage: %prog import [options] [ ...]" - parser = OptionParser(usage=get_usage_str(usage)) - parser.add_option("--link", action="store_true", - help="Attempt to hardlink instead of uploading") - parser.add_option("--test", action="store_true", help="Don't actually import") - parser.add_option("--create-build", action="store_true", help="Auto-create builds as needed") - parser.add_option("--src-epoch", help="When auto-creating builds, use this epoch") - (options, args) = parser.parse_args(args) - if len(args) < 1: - parser.error("At least one package must be specified") - if options.src_epoch in ('None', 'none', '(none)'): - options.src_epoch = None - elif options.src_epoch: - try: - options.src_epoch = int(options.src_epoch) - except (ValueError, TypeError): - parser.error("Invalid value for epoch: %s" % options.src_epoch) - activate_session(session, goptions) - to_import = {} - for path in args: - data = koji.get_header_fields(path, ('name', 'version', 'release', 'epoch', - 'arch', 'sigmd5', 'sourcepackage', 'sourcerpm')) - if data['sourcepackage']: - data['arch'] = 'src' - nvr = "%(name)s-%(version)s-%(release)s" % data - else: - nvr = "%(name)s-%(version)s-%(release)s" % koji.parse_NVRA(data['sourcerpm']) - to_import.setdefault(nvr, []).append((path, data)) - builds_missing = False - nvrs = sorted(to_list(to_import.keys())) - for nvr in nvrs: - to_import[nvr].sort() - for path, data in to_import[nvr]: - if data['sourcepackage']: - break - else: - # no srpm included, check for build - binfo = session.getBuild(nvr) - if not binfo: - print("Missing build or srpm: %s" % nvr) - builds_missing = True - if builds_missing and not options.create_build: - print("Aborting import") - return - - # local function to help us out below - def do_import(path, data): - rinfo = dict([(k, data[k]) for k in ('name', 'version', 'release', 'arch')]) - prev = session.getRPM(rinfo) - if prev and not prev.get('external_repo_id', 0): - if prev['payloadhash'] == koji.hex_string(data['sigmd5']): - print("RPM already imported: %s" % path) - else: - warn("md5sum mismatch for %s" % path) - warn(" A different rpm with the same name has already been imported") - warn(" Existing sigmd5 is %r, your import has %r" % ( - prev['payloadhash'], koji.hex_string(data['sigmd5']))) - print("Skipping import") - return - if options.test: - print("Test mode -- skipping import for %s" % path) - return - serverdir = unique_path('cli-import') - if options.link: - linked_upload(path, serverdir) - else: - sys.stdout.write("uploading %s... " % path) - sys.stdout.flush() - session.uploadWrapper(path, serverdir) - print("done") - sys.stdout.flush() - sys.stdout.write("importing %s... " % path) - sys.stdout.flush() - try: - session.importRPM(serverdir, os.path.basename(path)) - except koji.GenericError as e: - print("\nError importing: %s" % str(e).splitlines()[-1]) - sys.stdout.flush() - else: - print("done") - sys.stdout.flush() - - for nvr in nvrs: - # check for existing build - need_build = True - binfo = session.getBuild(nvr) - if binfo: - b_state = koji.BUILD_STATES[binfo['state']] - if b_state == 'COMPLETE': - need_build = False - elif b_state in ['FAILED', 'CANCELED']: - if not options.create_build: - print("Build %s state is %s. Skipping import" % (nvr, b_state)) - continue - else: - print("Build %s exists with state=%s. Skipping import" % (nvr, b_state)) - continue - - # import srpms first, if any - for path, data in to_import[nvr]: - if data['sourcepackage']: - if binfo and b_state != 'COMPLETE': - # need to fix the state - print("Creating empty build: %s" % nvr) - b_data = koji.util.dslice(binfo, ['name', 'version', 'release']) - b_data['epoch'] = data['epoch'] - session.createEmptyBuild(**b_data) - binfo = session.getBuild(nvr) - do_import(path, data) - need_build = False - - if need_build: - # if we're doing this here, we weren't given the matching srpm - if not options.create_build: # pragma: no cover - if binfo: - # should have caught this earlier, but just in case... - b_state = koji.BUILD_STATES[binfo['state']] - print("Build %s state is %s. Skipping import" % (nvr, b_state)) - continue - else: - print("No such build: %s (include matching srpm or use " - "--create-build option to add it)" % nvr) - continue - else: - # let's make a new build - b_data = koji.parse_NVR(nvr) - if options.src_epoch: - b_data['epoch'] = options.src_epoch - else: - # pull epoch from first rpm - data = to_import[nvr][0][1] - b_data['epoch'] = data['epoch'] - if options.test: - print("Test mode -- would have created empty build: %s" % nvr) - else: - print("Creating empty build: %s" % nvr) - session.createEmptyBuild(**b_data) - binfo = session.getBuild(nvr) - - for path, data in to_import[nvr]: - if data['sourcepackage']: - continue - do_import(path, data) - - diff --git a/cli/koji_cli/commands/cmd_import_archive.py b/cli/koji_cli/commands/cmd_import_archive.py deleted file mode 100644 index 3c55a15..0000000 --- a/cli/koji_cli/commands/cmd_import_archive.py +++ /dev/null @@ -1,118 +0,0 @@ -from __future__ import absolute_import, division - -import os -from optparse import OptionParser - - -import koji - -from koji_cli.lib import ( - _progress_callback, - _running_in_bg, - activate_session, - arg_filter, - get_usage_str, - linked_upload, - unique_path -) - - -def handle_import_archive(options, session, args): - "[admin] Import an archive file and associate it with a build" - usage = "usage: %prog import-archive [ 1: - parser.error("You must specify a build ID or N-V-R and an archive to import") - - activate_session(session, options) - - if not suboptions.type: - parser.error("You must specify an archive type") - if suboptions.type == 'maven': - if not (session.hasPerm('maven-import') or session.hasPerm('admin')): - parser.error("This action requires the maven-import privilege") - if not suboptions.type_info: - parser.error("--type-info must point to a .pom file when importing Maven archives") - pom_info = koji.parse_pom(suboptions.type_info) - maven_info = koji.pom_to_maven_info(pom_info) - suboptions.type_info = maven_info - elif suboptions.type == 'win': - if not (session.hasPerm('win-import') or session.hasPerm('admin')): - parser.error("This action requires the win-import privilege") - if not suboptions.type_info: - parser.error("--type-info must be specified") - type_info = suboptions.type_info.split(':', 2) - if len(type_info) < 2: - parser.error("--type-info must be in relpath:platforms[:flags] format") - win_info = {'relpath': type_info[0], 'platforms': type_info[1].split()} - if len(type_info) > 2: - win_info['flags'] = type_info[2].split() - else: - win_info['flags'] = [] - suboptions.type_info = win_info - elif suboptions.type == 'image': - if not (session.hasPerm('image-import') or session.hasPerm('admin')): - parser.error("This action requires the image-import privilege") - if not suboptions.type_info: - parser.error("--type-info must be specified") - image_info = {'arch': suboptions.type_info} - suboptions.type_info = image_info - else: - parser.error("Unsupported archive type: %s" % suboptions.type) - - buildinfo = session.getBuild(arg_filter(args[0])) - if not buildinfo: - if not suboptions.create_build: - parser.error("No such build: %s" % args[0]) - buildinfo = koji.parse_NVR(args[0]) - if buildinfo['epoch'] == '': - buildinfo['epoch'] = None - else: - buildinfo['epoch'] = int(buildinfo['epoch']) - if suboptions.type == 'maven': - # --type-info should point to a local .pom file - session.createMavenBuild(buildinfo, suboptions.type_info) - elif suboptions.type == 'win': - # We're importing, so we don't know what platform the build - # was run on. Use "import" as a placeholder. - session.createWinBuild(buildinfo, {'platform': 'import'}) - elif suboptions.type == 'image': - # --type-info should have an arch of the image - session.createImageBuild(buildinfo) - else: - # should get caught above - assert False # pragma: no cover - - for filepath in args[1:]: - filename = os.path.basename(filepath) - print("Uploading archive: %s" % filename) - serverdir = unique_path('cli-import') - if _running_in_bg() or suboptions.noprogress: - callback = None - else: - callback = _progress_callback - if suboptions.link: - linked_upload(filepath, serverdir) - else: - session.uploadWrapper(filepath, serverdir, callback=callback) - print('') - serverpath = "%s/%s" % (serverdir, filename) - session.importArchive(serverpath, buildinfo, suboptions.type, suboptions.type_info) - print("Imported: %s" % filename) - - diff --git a/cli/koji_cli/commands/cmd_import_cg.py b/cli/koji_cli/commands/cmd_import_cg.py deleted file mode 100644 index e7fbdd6..0000000 --- a/cli/koji_cli/commands/cmd_import_cg.py +++ /dev/null @@ -1,71 +0,0 @@ -from __future__ import absolute_import, division - -import os -from optparse import OptionParser - - -import koji - -from koji_cli.lib import ( - _progress_callback, - _running_in_bg, - activate_session, - error, - get_usage_str, - linked_upload, - unique_path -) - - -def handle_import_cg(goptions, session, args): - "[admin] Import external builds with rich metadata" - usage = "usage: %prog import-cg [options] " - parser = OptionParser(usage=get_usage_str(usage)) - parser.add_option("--noprogress", action="store_true", - help="Do not display progress of the upload") - parser.add_option("--link", action="store_true", - help="Attempt to hardlink instead of uploading") - parser.add_option("--test", action="store_true", help="Don't actually import") - parser.add_option("--token", action="store", default=None, help="Build reservation token") - (options, args) = parser.parse_args(args) - if len(args) < 2: - parser.error("Please specify metadata files directory") - activate_session(session, goptions) - metadata = koji.load_json(args[0]) - if 'output' not in metadata: - error("Metadata contains no output") - localdir = args[1] - - to_upload = [] - for info in metadata['output']: - if info.get('metadata_only', False): - continue - localpath = os.path.join(localdir, info.get('relpath', ''), info['filename']) - if not os.path.exists(localpath): - parser.error("No such file: %s" % localpath) - to_upload.append([localpath, info]) - - if options.test: - return - - # get upload path - # XXX - need a better way - serverdir = unique_path('cli-import') - - for localpath, info in to_upload: - relpath = os.path.join(serverdir, info.get('relpath', '')) - if _running_in_bg() or options.noprogress: - callback = None - else: - callback = _progress_callback - if options.link: - linked_upload(localpath, relpath) - else: - print("Uploading %s" % localpath) - session.uploadWrapper(localpath, relpath, callback=callback) - if callback: - print('') - - session.CGImport(metadata, serverdir, options.token) - - diff --git a/cli/koji_cli/commands/cmd_import_comps.py b/cli/koji_cli/commands/cmd_import_comps.py deleted file mode 100644 index f5f1195..0000000 --- a/cli/koji_cli/commands/cmd_import_comps.py +++ /dev/null @@ -1,106 +0,0 @@ -from __future__ import absolute_import, division - -from optparse import OptionParser - -import six -import six.moves.xmlrpc_client - - -from koji_cli.lib import ( - activate_session, - error, - get_usage_str -) - - -def handle_import_comps(goptions, session, args): - "Import group/package information from a comps file" - usage = "usage: %prog import-comps [options] " - parser = OptionParser(usage=get_usage_str(usage)) - parser.add_option("--force", action="store_true", help="force import") - (local_options, args) = parser.parse_args(args) - if len(args) != 2: - parser.error("Incorrect number of arguments") - activate_session(session, goptions) - # check if the tag exists - dsttag = session.getTag(args[1]) - if dsttag is None: - error("No such tag: %s" % args[1]) - if libcomps is not None: - _import_comps(session, args[0], args[1], local_options) - elif yumcomps is not None: - _import_comps_alt(session, args[0], args[1], local_options) - else: - error("comps module not available") - - -def _import_comps(session, filename, tag, options): - """Import comps data using libcomps module""" - comps = libcomps.Comps() - comps.fromxml_f(filename) - force = options.force - ptypes = { - libcomps.PACKAGE_TYPE_DEFAULT: 'default', - libcomps.PACKAGE_TYPE_OPTIONAL: 'optional', - libcomps.PACKAGE_TYPE_CONDITIONAL: 'conditional', - libcomps.PACKAGE_TYPE_MANDATORY: 'mandatory', - libcomps.PACKAGE_TYPE_UNKNOWN: 'unknown', - } - for group in comps.groups: - print("Group: %s (%s)" % (group.id, group.name)) - session.groupListAdd( - tag, group.id, force=force, display_name=group.name, - is_default=bool(group.default), - uservisible=bool(group.uservisible), - description=group.desc, - langonly=group.lang_only, - biarchonly=bool(group.biarchonly)) - for pkg in group.packages: - pkgopts = {'type': ptypes[pkg.type], - 'basearchonly': bool(pkg.basearchonly), - } - if pkg.type == libcomps.PACKAGE_TYPE_CONDITIONAL: - pkgopts['requires'] = pkg.requires - for k in pkgopts.keys(): - if six.PY2 and isinstance(pkgopts[k], unicode): # noqa: F821 - pkgopts[k] = str(pkgopts[k]) - s_opts = ', '.join(["'%s': %r" % (k, pkgopts[k]) for k in sorted(pkgopts.keys())]) - print(" Package: %s: {%s}" % (pkg.name, s_opts)) - session.groupPackageListAdd(tag, group.id, pkg.name, force=force, **pkgopts) - # libcomps does not support group dependencies - # libcomps does not support metapkgs - - -def _import_comps_alt(session, filename, tag, options): # no cover 3.x - """Import comps data using yum.comps module""" - print('WARN: yum.comps does not support the biarchonly of group and basearchonly of package') - comps = yumcomps.Comps() - comps.add(filename) - force = options.force - for group in comps.groups: - print("Group: %(groupid)s (%(name)s)" % vars(group)) - session.groupListAdd(tag, group.groupid, force=force, display_name=group.name, - is_default=bool(group.default), - uservisible=bool(group.user_visible), - description=group.description, - langonly=group.langonly) - # yum.comps does not support the biarchonly field - for ptype, pdata in [('mandatory', group.mandatory_packages), - ('default', group.default_packages), - ('optional', group.optional_packages), - ('conditional', group.conditional_packages)]: - for pkg in pdata: - # yum.comps does not support basearchonly - pkgopts = {'type': ptype} - if ptype == 'conditional': - pkgopts['requires'] = pdata[pkg] - for k in pkgopts.keys(): - if six.PY2 and isinstance(pkgopts[k], unicode): # noqa: F821 - pkgopts[k] = str(pkgopts[k]) - s_opts = ', '.join(["'%s': %r" % (k, pkgopts[k]) for k in sorted(pkgopts.keys())]) - print(" Package: %s: {%s}" % (pkg, s_opts)) - session.groupPackageListAdd(tag, group.groupid, pkg, force=force, **pkgopts) - # yum.comps does not support group dependencies - # yum.comps does not support metapkgs - - diff --git a/cli/koji_cli/commands/cmd_import_sig.py b/cli/koji_cli/commands/cmd_import_sig.py deleted file mode 100644 index 61307b3..0000000 --- a/cli/koji_cli/commands/cmd_import_sig.py +++ /dev/null @@ -1,84 +0,0 @@ -from __future__ import absolute_import, division - -import os -from optparse import SUPPRESS_HELP, OptionParser - - -import koji -from koji.util import base64encode, md5_constructor - -from koji_cli.lib import ( - activate_session, - get_usage_str, - warn -) - - -def handle_import_sig(goptions, session, args): - "[admin] Import signatures into the database and write signed RPMs" - usage = "usage: %prog import-sig [options] [ ...]" - parser = OptionParser(usage=get_usage_str(usage)) - parser.add_option("--with-unsigned", action="store_true", - help="Also import unsigned sig headers") - parser.add_option("--write", action="store_true", help=SUPPRESS_HELP) - parser.add_option("--test", action="store_true", help="Test mode -- don't actually import") - (options, args) = parser.parse_args(args) - if len(args) < 1: - parser.error("At least one package must be specified") - for path in args: - if not os.path.exists(path): - parser.error("No such file: %s" % path) - activate_session(session, goptions) - for path in args: - data = koji.get_header_fields(path, ('name', 'version', 'release', 'arch', 'siggpg', - 'sigpgp', 'dsaheader', 'rsaheader', - 'sourcepackage')) - if data['sourcepackage']: - data['arch'] = 'src' - sigkey = data['siggpg'] - if not sigkey: - sigkey = data['sigpgp'] - if not sigkey: - sigkey = data['dsaheader'] - if not sigkey: - sigkey = data['rsaheader'] - if not sigkey: - sigkey = "" - if not options.with_unsigned: - print("Skipping unsigned package: %s" % path) - continue - else: - sigkey = koji.get_sigpacket_key_id(sigkey) - del data['siggpg'] - del data['sigpgp'] - del data['dsaheader'] - del data['rsaheader'] - rinfo = session.getRPM(data) - if not rinfo: - print("No such rpm in system: %(name)s-%(version)s-%(release)s.%(arch)s" % data) - continue - if rinfo.get('external_repo_id'): - print("Skipping external rpm: %(name)s-%(version)s-%(release)s.%(arch)s@" - "%(external_repo_name)s" % rinfo) - continue - sighdr = koji.rip_rpm_sighdr(path) - previous = session.queryRPMSigs(rpm_id=rinfo['id'], sigkey=sigkey) - assert len(previous) <= 1 - if previous: - sighash = md5_constructor(sighdr).hexdigest() - if previous[0]['sighash'] == sighash: - print("Signature already imported: %s" % path) - continue - else: - warn("signature mismatch: %s" % path) - warn(" The system already has a signature for this rpm with key %s" % sigkey) - warn(" The two signature headers are not the same") - continue - print("Importing signature [key %s] from %s..." % (sigkey, path)) - if not options.test: - session.addRPMSig(rinfo['id'], base64encode(sighdr)) - print("Writing signed copy") - if not options.test: - session.writeSignedRPM(rinfo['id'], sigkey) - - diff --git a/cli/koji_cli/commands/cmd_latest_build.py b/cli/koji_cli/commands/cmd_latest_build.py deleted file mode 100644 index f3d1250..0000000 --- a/cli/koji_cli/commands/cmd_latest_build.py +++ /dev/null @@ -1,104 +0,0 @@ -from __future__ import absolute_import, division - -import os -import textwrap -from optparse import OptionParser - - -import koji - -from koji_cli.lib import ( - ensure_connection, - get_usage_str -) - - -def anon_handle_latest_build(goptions, session, args): - """[info] Print the latest builds for a tag""" - usage = """\ - usage: %prog latest-build [options] [ ...] - - The first option should be the name of a tag, not the name of a build target. - If you want to know the latest build in buildroots for a given build target, - then you should use the name of the build tag for that target. You can find - this value by running '%prog list-targets --name=' - - More information on tags and build targets can be found in the documentation. - https://docs.pagure.org/koji/HOWTO/#package-organization""" - - usage = textwrap.dedent(usage) - parser = OptionParser(usage=get_usage_str(usage)) - parser.add_option("--arch", help="List all of the latest packages for this arch") - parser.add_option("--all", action="store_true", - help="List all of the latest packages for this tag") - parser.add_option("--quiet", action="store_true", default=goptions.quiet, - help="Do not print the header information") - parser.add_option("--paths", action="store_true", help="Show the file paths") - parser.add_option("--type", - help="Show builds of the given type only. " - "Currently supported types: maven, win, image, or any custom " - "content generator btypes") - (options, args) = parser.parse_args(args) - if len(args) == 0: - parser.error("A tag name must be specified") - ensure_connection(session, goptions) - if options.all: - if len(args) > 1: - parser.error("A package name may not be combined with --all") - # Set None as the package argument - args.append(None) - else: - if len(args) < 2: - parser.error("A tag name and package name must be specified") - pathinfo = koji.PathInfo() - - for pkg in args[1:]: - if options.arch: - rpms, builds = session.getLatestRPMS(args[0], package=pkg, arch=options.arch) - builds_hash = dict([(x['build_id'], x) for x in builds]) - data = rpms - if options.paths: - for x in data: - z = x.copy() - x['name'] = builds_hash[x['build_id']]['package_name'] - x['path'] = os.path.join(pathinfo.build(x), pathinfo.rpm(z)) - fmt = "%(path)s" - else: - fmt = "%(name)s-%(version)s-%(release)s.%(arch)s" - else: - kwargs = {'package': pkg} - if options.type: - kwargs['type'] = options.type - data = session.getLatestBuilds(args[0], **kwargs) - if options.paths: - if options.type == 'maven': - for x in data: - x['path'] = pathinfo.mavenbuild(x) - fmt = "%(path)-40s %(tag_name)-20s %(maven_group_id)-20s " \ - "%(maven_artifact_id)-20s %(owner_name)s" - else: - for x in data: - x['path'] = pathinfo.build(x) - fmt = "%(path)-40s %(tag_name)-20s %(owner_name)s" - else: - if options.type == 'maven': - fmt = "%(nvr)-40s %(tag_name)-20s %(maven_group_id)-20s " \ - "%(maven_artifact_id)-20s %(owner_name)s" - else: - fmt = "%(nvr)-40s %(tag_name)-20s %(owner_name)s" - if not options.quiet: - if options.type == 'maven': - print("%-40s %-20s %-20s %-20s %s" % - ("Build", "Tag", "Group Id", "Artifact Id", "Built by")) - print("%s %s %s %s %s" % - ("-" * 40, "-" * 20, "-" * 20, "-" * 20, "-" * 16)) - else: - print("%-40s %-20s %s" % ("Build", "Tag", "Built by")) - print("%s %s %s" % ("-" * 40, "-" * 20, "-" * 16)) - options.quiet = True - - output = sorted([fmt % x for x in data]) - for line in output: - print(line) - - diff --git a/cli/koji_cli/commands/cmd_list_api.py b/cli/koji_cli/commands/cmd_list_api.py deleted file mode 100644 index 46a40d3..0000000 --- a/cli/koji_cli/commands/cmd_list_api.py +++ /dev/null @@ -1,44 +0,0 @@ -from __future__ import absolute_import, division - -from optparse import OptionParser - - - -from koji_cli.lib import ( - ensure_connection, - get_usage_str -) - - -def anon_handle_list_api(goptions, session, args): - "[info] Print the list of XML-RPC APIs" - usage = "usage: %prog list-api [options] [method_name ...]" - parser = OptionParser(usage=get_usage_str(usage)) - (options, args) = parser.parse_args(args) - ensure_connection(session, goptions) - if args: - for method in args: - help = session.system.methodHelp(method) - if not help: - parser.error("Unknown method: %s" % method) - print(help) - else: - for x in sorted(session._listapi(), key=lambda x: x['name']): - if 'argdesc' in x: - args = x['argdesc'] - elif x['args']: - # older servers may not provide argdesc - expanded = [] - for arg in x['args']: - if isinstance(arg, str): - expanded.append(arg) - else: - expanded.append('%s=%s' % (arg[0], arg[1])) - args = "(%s)" % ", ".join(expanded) - else: - args = "()" - print('%s%s' % (x['name'], args)) - if x['doc']: - print(" description: %s" % x['doc']) - - diff --git a/cli/koji_cli/commands/cmd_list_buildroot.py b/cli/koji_cli/commands/cmd_list_buildroot.py deleted file mode 100644 index 6d4ad71..0000000 --- a/cli/koji_cli/commands/cmd_list_buildroot.py +++ /dev/null @@ -1,42 +0,0 @@ -from __future__ import absolute_import, division - -from optparse import SUPPRESS_HELP, OptionParser - - - -from koji_cli.lib import ( - ensure_connection, - get_usage_str -) - - -def anon_handle_list_buildroot(goptions, session, args): - "[info] List the rpms used in or built in a buildroot" - usage = "usage: %prog list-buildroot [options] " - parser = OptionParser(usage=get_usage_str(usage)) - parser.add_option("--paths", action="store_true", help=SUPPRESS_HELP) - parser.add_option("--built", action="store_true", help="Show the built rpms") - parser.add_option("--verbose", "-v", action="store_true", help="Show more information") - (options, args) = parser.parse_args(args) - if len(args) != 1: - parser.error("Incorrect number of arguments") - ensure_connection(session, goptions) - if options.paths: - parser.error("--paths option is deprecated and will be removed in 1.30") - buildrootID = int(args[0]) - opts = {} - if options.built: - opts['buildrootID'] = buildrootID - else: - opts['componentBuildrootID'] = buildrootID - data = session.listRPMs(**opts) - - fmt = "%(nvr)s.%(arch)s" - order = sorted([(fmt % x, x) for x in data]) - for nvra, rinfo in order: - if options.verbose and rinfo.get('is_update'): - print("%s [update]" % nvra) - else: - print(nvra) - - diff --git a/cli/koji_cli/commands/cmd_list_builds.py b/cli/koji_cli/commands/cmd_list_builds.py deleted file mode 100644 index 328c167..0000000 --- a/cli/koji_cli/commands/cmd_list_builds.py +++ /dev/null @@ -1,145 +0,0 @@ -from __future__ import absolute_import, division - -from optparse import OptionParser - - -import koji - -from koji_cli.lib import ( - TimeOption, - ensure_connection, - get_usage_str, - warn -) - - -def anon_handle_list_builds(goptions, session, args): - "[info] Print the build listing" - usage = "usage: %prog list-builds [options]" - parser = OptionParser(usage=get_usage_str(usage), option_class=TimeOption) - parser.add_option("--package", help="List builds for this package") - parser.add_option("--buildid", help="List specific build from ID or nvr") - parser.add_option("--before", type="time", - help="List builds built before this time, " + TimeOption.get_help()) - parser.add_option("--after", type="time", - help="List builds built after this time (same format as for --before") - parser.add_option("--state", help="List builds in this state") - parser.add_option("--task", help="List builds for this task") - parser.add_option("--type", help="List builds of this type.") - parser.add_option("--prefix", help="Only builds starting with this prefix") - parser.add_option("--pattern", help="Only list builds matching this GLOB pattern") - parser.add_option("--cg", help="Only list builds imported by matching content generator name") - parser.add_option("--source", help="Only builds where the source field matches (glob pattern)") - parser.add_option("--owner", help="List builds built by this owner") - parser.add_option("--volume", help="List builds by volume ID") - parser.add_option("-k", "--sort-key", action="append", metavar='FIELD', - default=[], help="Sort the list by the named field. Allowed sort keys: " - "nvr, owner_name, state") - parser.add_option("-r", "--reverse", action="store_true", default=False, - help="Print the list in reverse order") - parser.add_option("--quiet", action="store_true", default=goptions.quiet, - help="Do not print the header information") - (options, args) = parser.parse_args(args) - if len(args) != 0: - parser.error("This command takes no arguments") - ensure_connection(session, goptions) - opts = {} - for key in ('type', 'prefix', 'pattern'): - value = getattr(options, key) - if value is not None: - opts[key] = value - if options.cg: - opts['cgID'] = options.cg - if options.package: - try: - opts['packageID'] = int(options.package) - except ValueError: - package = session.getPackageID(options.package) - if package is None: - parser.error("No such package: %s" % options.package) - opts['packageID'] = package - if options.owner: - try: - opts['userID'] = int(options.owner) - except ValueError: - user = session.getUser(options.owner) - if user is None: - parser.error("No such user: %s" % options.owner) - opts['userID'] = user['id'] - if options.volume: - try: - opts['volumeID'] = int(options.volume) - except ValueError: - volumes = session.listVolumes() - volumeID = None - for volume in volumes: - if options.volume == volume['name']: - volumeID = volume['id'] - if volumeID is None: - parser.error("No such volume: %s" % options.volume) - opts['volumeID'] = volumeID - if options.state: - try: - state = int(options.state) - if state > 4 or state < 0: - parser.error("Invalid state: %s" % options.state) - opts['state'] = state - except ValueError: - try: - opts['state'] = koji.BUILD_STATES[options.state] - except KeyError: - parser.error("Invalid state: %s" % options.state) - if options.before: - opts['completeBefore'] = options.before - if options.after: - opts['completeAfter'] = options.after - if options.task: - try: - opts['taskID'] = int(options.task) - except ValueError: - parser.error("Task id must be an integer") - if options.source: - opts['source'] = options.source - if options.buildid: - try: - buildid = int(options.buildid) - except ValueError: - buildid = options.buildid - data = [session.getBuild(buildid)] - if data[0] is None: - parser.error("No such build: '%s'" % buildid) - else: - # Check filter exists - if any(opts): - try: - data = session.listBuilds(**opts) - except koji.ParameterError as e: - if e.args[0].endswith("'pattern'"): - parser.error("The hub doesn't support the 'pattern' argument, please try" - " filtering the result on your local instead.") - if e.args[0].endswith("'cgID'"): - parser.error("The hub doesn't support the 'cg' argument, please try" - " filtering the result on your local instead.") - else: - parser.error("Filter must be provided for list") - if not options.sort_key: - options.sort_key = ['nvr'] - else: - for s_key in options.sort_key: - if s_key not in ['nvr', 'owner_name', 'state']: - warn("Invalid sort_key: %s." % s_key) - - data = sorted(data, key=lambda b: [b.get(k) for k in options.sort_key], - reverse=options.reverse) - for build in data: - build['state'] = koji.BUILD_STATES[build['state']] - - fmt = "%(nvr)-55s %(owner_name)-16s %(state)s" - if not options.quiet: - print("%-55s %-16s %s" % ("Build", "Built by", "State")) - print("%s %s %s" % ("-" * 55, "-" * 16, "-" * 16)) - - for build in data: - print(fmt % build) - - diff --git a/cli/koji_cli/commands/cmd_list_channels.py b/cli/koji_cli/commands/cmd_list_channels.py deleted file mode 100644 index 1f8fac9..0000000 --- a/cli/koji_cli/commands/cmd_list_channels.py +++ /dev/null @@ -1,95 +0,0 @@ -from __future__ import absolute_import, division - -from optparse import OptionParser - -from six.moves import zip - -import koji - -from koji_cli.lib import ( - ensure_connection, - get_usage_str, - truncate_string -) - - -def anon_handle_list_channels(goptions, session, args): - "[info] Print channels listing" - usage = "usage: %prog list-channels [options]" - parser = OptionParser(usage=get_usage_str(usage)) - parser.add_option("--simple", action="store_true", default=False, - help="Print just list of channels without additional info") - parser.add_option("--quiet", action="store_true", default=goptions.quiet, - help="Do not print header information") - parser.add_option("--comment", action="store_true", help="Show comments") - parser.add_option("--description", action="store_true", help="Show descriptions") - parser.add_option("--enabled", action="store_true", help="Limit to enabled channels") - parser.add_option("--not-enabled", action="store_false", dest="enabled", - help="Limit to not enabled channels") - parser.add_option("--disabled", action="store_false", dest="enabled", - help="Alias for --not-enabled") - (options, args) = parser.parse_args(args) - ensure_connection(session, goptions) - opts = {} - if options.enabled is not None: - opts['enabled'] = options.enabled - try: - channels = sorted([x for x in session.listChannels(**opts)], key=lambda x: x['name']) - except koji.ParameterError: - channels = sorted([x for x in session.listChannels()], key=lambda x: x['name']) - if len(channels) > 0: - first_item = channels[0] - else: - first_item = {} - session.multicall = True - for channel in channels: - session.listHosts(channelID=channel['id']) - for channel, [hosts] in zip(channels, session.multiCall()): - channel['enabled_host'] = len([x for x in hosts if x['enabled']]) - channel['disabled'] = len(hosts) - channel['enabled_host'] - channel['ready'] = len([x for x in hosts if x['ready']]) - channel['capacity'] = sum([x['capacity'] for x in hosts]) - channel['load'] = sum([x['task_load'] for x in hosts]) - if 'comment' in first_item: - channel['comment'] = truncate_string(channel['comment']) - if 'description' in first_item: - channel['description'] = truncate_string(channel['description']) - if channel['capacity']: - channel['perc_load'] = channel['load'] / channel['capacity'] * 100.0 - else: - channel['perc_load'] = 0.0 - if 'enabled' in first_item: - if not channel['enabled']: - channel['name'] = channel['name'] + ' [disabled]' - if channels: - longest_channel = max([len(ch['name']) for ch in channels]) - else: - longest_channel = 8 - if options.simple: - if not options.quiet: - hdr = 'Channel' - print(hdr) - print(len(hdr) * '-') - for channel in channels: - print(channel['name']) - else: - if not options.quiet: - hdr = '{channame:<{longest_channel}}Enabled Ready Disbld Load Cap ' \ - 'Perc ' - hdr = hdr.format(longest_channel=longest_channel, channame='Channel') - if options.description and 'description' in first_item: - hdr += "Description".ljust(53) - if options.comment and 'comment' in first_item: - hdr += "Comment".ljust(53) - print(hdr) - print(len(hdr) * '-') - mask = "%%(name)-%ss %%(enabled_host)6d %%(ready)6d %%(disabled)6d %%(load)6d %%(" \ - "capacity)6d %%(perc_load)6d%%%%" % longest_channel - if options.description and 'description' in first_item: - mask += " %(description)-50s" - if options.comment and 'comment' in first_item: - mask += " %(comment)-50s" - for channel in channels: - print(mask % channel) - - diff --git a/cli/koji_cli/commands/cmd_list_external_repos.py b/cli/koji_cli/commands/cmd_list_external_repos.py deleted file mode 100644 index d37f2bd..0000000 --- a/cli/koji_cli/commands/cmd_list_external_repos.py +++ /dev/null @@ -1,117 +0,0 @@ -from __future__ import absolute_import, division - -import time -from optparse import OptionParser - - -import koji - -from koji_cli.lib import ( - ensure_connection, - get_usage_str -) - - -def anon_handle_list_external_repos(goptions, session, args): - "[info] List external repos" - usage = "usage: %prog list-external-repos [options]" - parser = OptionParser(usage=get_usage_str(usage)) - parser.add_option("--url", help="Select by url") - parser.add_option("--name", help="Select by name") - parser.add_option("--id", type="int", help="Select by id") - parser.add_option("--tag", help="Select by tag") - parser.add_option("--used", action='store_true', help="List which tags use the repo(s)") - parser.add_option("--inherit", action='store_true', - help="Follow tag inheritance when selecting by tag") - parser.add_option("--event", type='int', metavar="EVENT#", help="Query at event") - parser.add_option("--ts", type='int', metavar="TIMESTAMP", - help="Query at last event before timestamp") - parser.add_option("--repo", type='int', metavar="REPO#", - help="Query at event corresponding to (nonexternal) repo") - parser.add_option("--quiet", action="store_true", default=goptions.quiet, - help="Do not display the column headers") - (options, args) = parser.parse_args(args) - if len(args) > 0: - parser.error("This command takes no arguments") - ensure_connection(session, goptions) - opts = {} - event = koji.util.eventFromOpts(session, options) - if event: - opts['event'] = event['id'] - event['timestr'] = time.asctime(time.localtime(event['ts'])) - print("Querying at event %(id)i (%(timestr)s)" % event) - if options.tag: - format = "tag" - opts['tag_info'] = options.tag - opts['repo_info'] = options.id or options.name or None - if opts['repo_info']: - if options.inherit: - parser.error("Can't select by repo when using --inherit") - if options.inherit: - del opts['repo_info'] - data = session.getExternalRepoList(**opts) - format = "multitag" - else: - data = session.getTagExternalRepos(**opts) - elif options.used: - format = "multitag" - opts['repo_info'] = options.id or options.name or None - data = session.getTagExternalRepos(**opts) - else: - format = "basic" - opts['info'] = options.id or options.name or None - opts['url'] = options.url or None - data = session.listExternalRepos(**opts) - - # There are three different output formats - # 1) Listing just repo data (name, url) - # 2) Listing repo data for a tag (priority, name, url) - # 3) Listing repo data for multiple tags (tag, priority, name, url) - if format == "basic": - format = "%(name)-25s %(url)s" - header1 = "%-25s %s" % ("External repo name", "URL") - header2 = "%s %s" % ("-" * 25, "-" * 40) - elif format == "tag": - format = "%(priority)-3i %(external_repo_name)-25s %(merge_mode)-10s %(url)s" - header1 = "%-3s %-25s %-10s URL" % ("Pri", "External repo name", "Mode") - header2 = "%s %s %s %s" % ("-" * 3, "-" * 25, "-" * 10, "-" * 40) - elif format == "multitag": - format = "%(tag_name)-20s %(priority)-3i %(merge_mode)-10s %(external_repo_name)s" - header1 = "%-20s %-3s %-10s %s" % ("Tag", "Pri", "Mode", "External repo name") - header2 = "%s %s %s %s" % ("-" * 20, "-" * 3, "-" * 10, "-" * 25) - if not options.quiet: - print(header1) - print(header2) - for rinfo in data: - # older hubs do not support merge_mode - rinfo.setdefault('merge_mode', None) - print(format % rinfo) - - -def _pick_external_repo_priority(session, tag): - """pick priority after current ones, leaving space for later insertions""" - repolist = session.getTagExternalRepos(tag_info=tag) - # ordered by priority - if not repolist: - priority = 5 - else: - priority = (repolist[-1]['priority'] + 7) // 5 * 5 - # at least 3 higher than current max and a multiple of 5 - return priority - - -def _parse_tagpri(tagpri): - parts = tagpri.rsplit('::', 1) - tag = parts[0] - if len(parts) == 1: - return tag, None - elif parts[1] in ('auto', '-1'): - return tag, None - else: - try: - pri = int(parts[1]) - except ValueError: - raise koji.GenericError("Invalid priority: %s" % parts[1]) - return tag, pri - - diff --git a/cli/koji_cli/commands/cmd_list_groups.py b/cli/koji_cli/commands/cmd_list_groups.py deleted file mode 100644 index dc660a5..0000000 --- a/cli/koji_cli/commands/cmd_list_groups.py +++ /dev/null @@ -1,64 +0,0 @@ -from __future__ import absolute_import, division - -import time -from optparse import OptionParser - - -import koji - -from koji_cli.lib import ( - ensure_connection, - get_usage_str -) - - -def anon_handle_list_groups(goptions, session, args): - "[info] Print the group listings" - usage = "usage: %prog list-groups [options] []" - parser = OptionParser(usage=get_usage_str(usage)) - parser.add_option("--event", type='int', metavar="EVENT#", help="query at event") - parser.add_option("--ts", type='int', metavar="TIMESTAMP", - help="query at last event before timestamp") - parser.add_option("--repo", type='int', metavar="REPO#", help="query at event for a repo") - parser.add_option("--show-blocked", action="store_true", dest="incl_blocked", - help="Show blocked packages and groups") - (options, args) = parser.parse_args(args) - if len(args) < 1 or len(args) > 2: - parser.error("Incorrect number of arguments") - opts = {} - if options.incl_blocked: - opts['incl_blocked'] = True - ensure_connection(session, goptions) - event = koji.util.eventFromOpts(session, options) - if event: - opts['event'] = event['id'] - event['timestr'] = time.asctime(time.localtime(event['ts'])) - print("Querying at event %(id)i (%(timestr)s)" % event) - tmp_list = sorted([(x['name'], x) for x in session.getTagGroups(args[0], **opts)]) - groups = [x[1] for x in tmp_list] - - tags_cache = {} - - def get_cached_tag(tag_id): - if tag_id not in tags_cache: - tag = session.getTag(tag_id, strict=False) - if tag is None: - tags_cache[tag_id] = tag_id - else: - tags_cache[tag_id] = tag['name'] - return tags_cache[tag_id] - - for group in groups: - if len(args) > 1 and group['name'] != args[1]: - continue - print("%s [%s]" % (group['name'], get_cached_tag(group['tag_id']))) - groups = sorted([(x['name'], x) for x in group['grouplist']]) - for x in [x[1] for x in groups]: - x['tag_name'] = get_cached_tag(x['tag_id']) - print_group_list_req_group(x) - pkgs = sorted([(x['package'], x) for x in group['packagelist']]) - for x in [x[1] for x in pkgs]: - x['tag_name'] = get_cached_tag(x['tag_id']) - print_group_list_req_package(x) - - diff --git a/cli/koji_cli/commands/cmd_list_history.py b/cli/koji_cli/commands/cmd_list_history.py deleted file mode 100644 index 10e4e3f..0000000 --- a/cli/koji_cli/commands/cmd_list_history.py +++ /dev/null @@ -1,412 +0,0 @@ -from __future__ import absolute_import, division - -import logging -import os -import sys -import time -import traceback -from optparse import SUPPRESS_HELP, OptionParser - -from six.moves import zip - -import koji - -from koji_cli.lib import ( - TimeOption, - ensure_connection, - error, - get_usage_str, - list_task_output_all_volumes -) - - -def anon_handle_list_history(goptions, session, args): - "[info] Display historical data" - usage = "usage: %prog list-history [options]" - parser = OptionParser(usage=get_usage_str(usage), option_class=TimeOption) - # Don't use local debug option, this one stays here for backward compatibility - # https://pagure.io/koji/issue/2084 - parser.add_option("--debug", action="store_true", default=goptions.debug, help=SUPPRESS_HELP) - parser.add_option("--build", help="Only show data for a specific build") - parser.add_option("--package", help="Only show data for a specific package") - parser.add_option("--tag", help="Only show data for a specific tag") - parser.add_option("--editor", "--by", metavar="USER", - help="Only show entries modified by user") - parser.add_option("--user", help="Only show entries affecting a user") - parser.add_option("--permission", help="Only show entries relating to a given permission") - parser.add_option("--cg", help="Only show entries relating to a given content generator") - parser.add_option("--external-repo", "--erepo", - help="Only show entries relating to a given external repo") - parser.add_option("--build-target", "--target", - help="Only show entries relating to a given build target") - parser.add_option("--group", help="Only show entries relating to a given group") - parser.add_option("--host", help="Only show entries related to given host") - parser.add_option("--channel", help="Only show entries related to given channel") - parser.add_option("--xkey", help="Only show entries related to given tag extra key") - parser.add_option("--before", type="time", - help="Only show entries before this time, " + TimeOption.get_help()) - parser.add_option("--after", type="time", - help="Only show entries after timestamp (same format as for --before)") - parser.add_option("--before-event", metavar="EVENT_ID", type='int', - help="Only show entries before event") - parser.add_option("--after-event", metavar="EVENT_ID", type='int', - help="Only show entries after event") - parser.add_option("--watch", action="store_true", help="Monitor history data") - parser.add_option("--active", action='store_true', - help="Only show entries that are currently active") - parser.add_option("--revoked", action='store_false', dest='active', - help="Only show entries that are currently revoked") - parser.add_option("--context", action="store_true", help="Show related entries") - parser.add_option("-s", "--show", action="append", help="Show data from selected tables") - parser.add_option("-v", "--verbose", action="store_true", help="Show more detail") - parser.add_option("-e", "--events", action="store_true", help="Show event ids") - parser.add_option("--all", action="store_true", - help="Allows listing the entire global history") - parser.add_option("--utc", action="store_true", - help="Shows time in UTC timezone") - (options, args) = parser.parse_args(args) - if len(args) != 0: - parser.error("This command takes no arguments") - kwargs = {} - limited = False - for opt in ('package', 'tag', 'build', 'editor', 'user', 'permission', - 'cg', 'external_repo', 'build_target', 'group', 'before', - 'after', 'host', 'channel', 'xkey'): - val = getattr(options, opt) - if val: - kwargs[opt] = val - limited = True - if options.before_event: - kwargs['beforeEvent'] = options.before_event - if options.after_event: - kwargs['afterEvent'] = options.after_event - if options.active is not None: - kwargs['active'] = options.active - if options.host: - hostinfo = session.getHost(options.host) - if not hostinfo: - error("No such host: %s" % options.host) - if options.channel: - channelinfo = session.getChannel(options.channel) - if not channelinfo: - error("No such channel: %s" % options.channel) - if options.utc: - kwargs['utc'] = options.utc - tables = None - if options.show: - tables = [] - for arg in options.show: - tables.extend(arg.split(',')) - if not limited and not options.all: - parser.error("Please specify an option to limit the query") - - ensure_connection(session, goptions) - - if options.watch: - if not kwargs.get('afterEvent') and not kwargs.get('after'): - kwargs['afterEvent'] = session.getLastEvent()['id'] - - while True: - histdata = session.queryHistory(tables=tables, **kwargs) - timeline = [] - - def distinguish_match(x, name): - """determine if create or revoke event matched""" - if options.context: - return True - name = "_" + name - ret = True - for key in x: - if key.startswith(name): - ret = ret and x[key] - return ret - for table in histdata: - hist = histdata[table] - for x in hist: - if x['revoke_event'] is not None: - if distinguish_match(x, 'revoked'): - timeline.append((x['revoke_event'], table, 0, x.copy())) - # pprint.pprint(timeline[-1]) - if distinguish_match(x, 'created'): - timeline.append((x['create_event'], table, 1, x)) - timeline.sort(key=lambda entry: entry[:3]) - # group edits together - new_timeline = [] - last_event = None - edit_index = {} - for entry in timeline: - event_id, table, create, x = entry - if event_id != last_event: - edit_index = {} - last_event = event_id - key = tuple([x[k] for k in _table_keys[table]]) - prev = edit_index.get((table, event_id), {}).get(key) - if prev: - prev[-1].setdefault('.related', []).append(entry) - else: - edit_index.setdefault((table, event_id), {})[key] = entry - new_timeline.append(entry) - for entry in new_timeline: - if options.debug: - print("%r" % list(entry)) - _print_histline(entry, options=options) - if not options.watch: - break - else: - time.sleep(goptions.poll_interval) - # repeat query for later events - if last_event: - kwargs['afterEvent'] = last_event - - -def _handleMap(lines, data, prefix=''): - for key, val in data.items(): - if key != '__starstar': - lines.append(' %s%s: %s' % (prefix, key, val)) - - -def _handleOpts(lines, opts, prefix=''): - if opts: - lines.append("%sOptions:" % prefix) - _handleMap(lines, opts, prefix) - - -def _parseTaskParams(session, method, task_id, topdir): - try: - return _do_parseTaskParams(session, method, task_id, topdir) - except Exception: - logger = logging.getLogger("koji") - if logger.isEnabledFor(logging.DEBUG): - tb_str = ''.join(traceback.format_exception(*sys.exc_info())) - logger.debug(tb_str) - return ['Unable to parse task parameters'] - - -def _do_parseTaskParams(session, method, task_id, topdir): - """Parse the return of getTaskRequest()""" - params = session.getTaskRequest(task_id) - - lines = [] - - if method == 'buildSRPMFromCVS': - lines.append("CVS URL: %s" % params[0]) - elif method == 'buildSRPMFromSCM': - lines.append("SCM URL: %s" % params[0]) - elif method == 'buildArch': - lines.append("SRPM: %s/work/%s" % (topdir, params[0])) - lines.append("Build Tag: %s" % session.getTag(params[1])['name']) - lines.append("Build Arch: %s" % params[2]) - lines.append("SRPM Kept: %r" % params[3]) - if len(params) > 4: - _handleOpts(lines, params[4]) - elif method == 'tagBuild': - build = session.getBuild(params[1]) - lines.append("Destination Tag: %s" % session.getTag(params[0])['name']) - lines.append("Build: %s" % koji.buildLabel(build)) - elif method == 'buildNotification': - build = params[1] - buildTarget = params[2] - lines.append("Recipients: %s" % (", ".join(params[0]))) - lines.append("Build: %s" % koji.buildLabel(build)) - lines.append("Build Target: %s" % buildTarget['name']) - lines.append("Web URL: %s" % params[3]) - elif method == 'build': - lines.append("Source: %s" % params[0]) - lines.append("Build Target: %s" % params[1]) - if len(params) > 2: - _handleOpts(lines, params[2]) - elif method == 'maven': - lines.append("SCM URL: %s" % params[0]) - lines.append("Build Target: %s" % params[1]) - if len(params) > 2: - _handleOpts(lines, params[2]) - elif method == 'buildMaven': - lines.append("SCM URL: %s" % params[0]) - lines.append("Build Tag: %s" % params[1]['name']) - if len(params) > 2: - _handleOpts(lines, params[2]) - elif method == 'wrapperRPM': - lines.append("Spec File URL: %s" % params[0]) - lines.append("Build Tag: %s" % params[1]['name']) - if params[2]: - lines.append("Build: %s" % koji.buildLabel(params[2])) - if params[3]: - lines.append("Task: %s %s" % (params[3]['id'], koji.taskLabel(params[3]))) - if len(params) > 4: - _handleOpts(lines, params[4]) - elif method == 'chainmaven': - lines.append("Builds:") - for package, opts in params[0].items(): - lines.append(" " + package) - _handleMap(lines, opts, prefix=" ") - lines.append("Build Target: %s" % params[1]) - if len(params) > 2: - _handleOpts(lines, params[2]) - elif method == 'winbuild': - lines.append("VM: %s" % params[0]) - lines.append("SCM URL: %s" % params[1]) - lines.append("Build Target: %s" % params[2]) - if len(params) > 3: - _handleOpts(lines, params[3]) - elif method == 'vmExec': - lines.append("VM: %s" % params[0]) - lines.append("Exec Params:") - for info in params[1]: - if isinstance(info, dict): - _handleMap(lines, info, prefix=' ') - else: - lines.append(" %s" % info) - if len(params) > 2: - _handleOpts(lines, params[2]) - elif method in ('createLiveCD', 'createAppliance', 'createLiveMedia'): - argnames = ['Name', 'Version', 'Release', 'Arch', 'Target Info', 'Build Tag', 'Repo', - 'Kickstart File'] - for n, v in zip(argnames, params): - lines.append("%s: %s" % (n, v)) - if len(params) > 8: - _handleOpts(lines, params[8]) - elif method in ('appliance', 'livecd', 'livemedia'): - argnames = ['Name', 'Version', 'Arches', 'Target', 'Kickstart'] - for n, v in zip(argnames, params): - lines.append("%s: %s" % (n, v)) - if len(params) > 5: - _handleOpts(lines, params[5]) - elif method == 'newRepo': - tag = session.getTag(params[0]) - lines.append("Tag: %s" % tag['name']) - elif method == 'prepRepo': - lines.append("Tag: %s" % params[0]['name']) - elif method == 'createrepo': - lines.append("Repo ID: %i" % params[0]) - lines.append("Arch: %s" % params[1]) - oldrepo = params[2] - if oldrepo: - lines.append("Old Repo ID: %i" % oldrepo['id']) - lines.append("Old Repo Creation: %s" % koji.formatTimeLong(oldrepo['create_ts'])) - if len(params) > 3: - lines.append("External Repos: %s" % - ', '.join([ext['external_repo_name'] for ext in params[3]])) - elif method == 'tagNotification': - destTag = session.getTag(params[2]) - srcTag = None - if params[3]: - srcTag = session.getTag(params[3]) - build = session.getBuild(params[4]) - user = session.getUser(params[5]) - - lines.append("Recipients: %s" % ", ".join(params[0])) - lines.append("Successful?: %s" % (params[1] and 'yes' or 'no')) - lines.append("Tagged Into: %s" % destTag['name']) - if srcTag: - lines.append("Moved From: %s" % srcTag['name']) - lines.append("Build: %s" % koji.buildLabel(build)) - lines.append("Tagged By: %s" % user['name']) - lines.append("Ignore Success?: %s" % (params[6] and 'yes' or 'no')) - if params[7]: - lines.append("Failure Message: %s" % params[7]) - elif method == 'dependantTask': - lines.append("Dependant Tasks: %s" % ", ".join([str(depID) for depID in params[0]])) - lines.append("Subtasks:") - for subtask in params[1]: - lines.append(" Method: %s" % subtask[0]) - lines.append(" Parameters: %s" % - ", ".join([str(subparam) for subparam in subtask[1]])) - if len(subtask) > 2 and subtask[2]: - subopts = subtask[2] - _handleOpts(lines, subopts, prefix=' ') - lines.append("") - elif method == 'chainbuild': - lines.append("Build Groups:") - group_num = 0 - for group_list in params[0]: - group_num += 1 - lines.append(" %i: %s" % (group_num, ', '.join(group_list))) - lines.append("Build Target: %s" % params[1]) - if len(params) > 2: - _handleOpts(lines, params[2]) - elif method == 'waitrepo': - lines.append("Build Target: %s" % params[0]) - if params[1]: - lines.append("Newer Than: %s" % params[1]) - if params[2]: - lines.append("NVRs: %s" % ', '.join(params[2])) - - return lines - - -def _printTaskInfo(session, task_id, topdir, level=0, recurse=True, verbose=True): - """Recursive function to print information about a task - and its children.""" - - BUILDDIR = '/var/lib/mock' - indent = " " * 2 * level - - info = session.getTaskInfo(task_id) - - if info is None: - raise koji.GenericError("No such task: %d" % task_id) - - if info['host_id']: - host_info = session.getHost(info['host_id']) - else: - host_info = None - buildroot_infos = session.listBuildroots(taskID=task_id) - build_info = session.listBuilds(taskID=task_id) - - files = list_task_output_all_volumes(session, task_id) - logs = [] - output = [] - for filename in files: - if filename.endswith('.log'): - logs += [os.path.join(koji.pathinfo.work(volume=volume), - koji.pathinfo.taskrelpath(task_id), - filename) for volume in files[filename]] - else: - output += [os.path.join(koji.pathinfo.work(volume=volume), - koji.pathinfo.taskrelpath(task_id), - filename) for volume in files[filename]] - - owner = session.getUser(info['owner'])['name'] - - print("%sTask: %d" % (indent, task_id)) - print("%sType: %s" % (indent, info['method'])) - if verbose: - print("%sRequest Parameters:" % indent) - for line in _parseTaskParams(session, info['method'], task_id, topdir): - print("%s %s" % (indent, line)) - print("%sOwner: %s" % (indent, owner)) - print("%sState: %s" % (indent, koji.TASK_STATES[info['state']].lower())) - print("%sCreated: %s" % (indent, time.asctime(time.localtime(info['create_ts'])))) - if info.get('start_ts'): - print("%sStarted: %s" % (indent, time.asctime(time.localtime(info['start_ts'])))) - if info.get('completion_ts'): - print("%sFinished: %s" % (indent, time.asctime(time.localtime(info['completion_ts'])))) - if host_info: - print("%sHost: %s" % (indent, host_info['name'])) - if build_info: - print("%sBuild: %s (%d)" % (indent, build_info[0]['nvr'], build_info[0]['build_id'])) - if buildroot_infos: - print("%sBuildroots:" % indent) - for root in buildroot_infos: - print("%s %s/%s-%d-%d/" % - (indent, BUILDDIR, root['tag_name'], root['id'], root['repo_id'])) - if logs: - print("%sLog Files:" % indent) - for log_path in logs: - print("%s %s" % (indent, log_path)) - if output: - print("%sOutput:" % indent) - for file_path in output: - print("%s %s" % (indent, file_path)) - - # white space - print('') - - if recurse: - level += 1 - children = session.getTaskChildren(task_id, request=True) - children.sort(key=lambda x: x['id']) - for child in children: - _printTaskInfo(session, child['id'], topdir, level, verbose=verbose) - - diff --git a/cli/koji_cli/commands/cmd_list_hosts.py b/cli/koji_cli/commands/cmd_list_hosts.py deleted file mode 100644 index d404431..0000000 --- a/cli/koji_cli/commands/cmd_list_hosts.py +++ /dev/null @@ -1,132 +0,0 @@ -from __future__ import absolute_import, division - -from optparse import OptionParser - -from six.moves import zip - -import koji - -from koji_cli.lib import ( - ensure_connection, - get_usage_str, - warn, - truncate_string -) - - -def anon_handle_list_hosts(goptions, session, args): - "[info] Print the host listing" - usage = "usage: %prog list-hosts [options]" - parser = OptionParser(usage=get_usage_str(usage)) - parser.add_option("--arch", action="append", default=[], help="Specify an architecture") - parser.add_option("--channel", help="Specify a channel") - parser.add_option("--ready", action="store_true", help="Limit to ready hosts") - parser.add_option("--not-ready", action="store_false", dest="ready", - help="Limit to not ready hosts") - parser.add_option("--enabled", action="store_true", help="Limit to enabled hosts") - parser.add_option("--not-enabled", action="store_false", dest="enabled", - help="Limit to not enabled hosts") - parser.add_option("--disabled", action="store_false", dest="enabled", - help="Alias for --not-enabled") - parser.add_option("--quiet", action="store_true", default=goptions.quiet, - help="Do not print header information") - parser.add_option("--show-channels", action="store_true", help="Show host's channels") - parser.add_option("--comment", action="store_true", help="Show comments") - parser.add_option("--description", action="store_true", help="Show descriptions") - (options, args) = parser.parse_args(args) - opts = {} - ensure_connection(session, goptions) - if options.arch: - opts['arches'] = options.arch - if options.channel: - channel = session.getChannel(options.channel) - if not channel: - parser.error('No such channel: %s' % options.channel) - opts['channelID'] = channel['id'] - if options.ready is not None: - opts['ready'] = options.ready - if options.enabled is not None: - opts['enabled'] = options.enabled - tmp_list = sorted([(x['name'], x) for x in session.listHosts(**opts)]) - hosts = [x[1] for x in tmp_list] - - if not hosts: - warn("No hosts found.") - return - - def yesno(x): - if x: - return 'Y' - else: - return 'N' - - try: - first = session.getLastHostUpdate(hosts[0]['id'], ts=True) - opts = {'ts': True} - except koji.ParameterError: - # Hubs prior to v1.25.0 do not have a "ts" parameter for getLastHostUpdate - first = session.getLastHostUpdate(hosts[0]['id']) - opts = {} - - # pull in the last update using multicall to speed it up a bit - with session.multicall() as m: - result = [m.getLastHostUpdate(host['id'], **opts) for host in hosts[1:]] - updateList = [first] + [x.result for x in result] - - for host, update in zip(hosts, updateList): - if update is None: - host['update'] = '-' - else: - host['update'] = koji.formatTimeLong(update) - host['enabled'] = yesno(host['enabled']) - host['ready'] = yesno(host['ready']) - host['arches'] = ','.join(host['arches'].split()) - host['description'] = truncate_string(host['description']) - host['comment'] = truncate_string(host['comment']) - - # pull hosts' channels - if options.show_channels: - with session.multicall() as m: - result = [m.listChannels(host['id']) for host in hosts] - first_line_channel = result[0].result[0] - for host, channels in zip(hosts, result): - list_channels = [] - for c in channels.result: - if 'enabled' in first_line_channel: - if c['enabled']: - list_channels.append(c['name']) - else: - list_channels.append('*' + c['name']) - else: - list_channels.append(c['name']) - host['channels'] = ','.join(sorted(list_channels)) - - if hosts: - longest_host = max([len(h['name']) for h in hosts]) - else: - longest_host = 8 - - if not options.quiet: - hdr = "{hostname:<{longest_host}} Enb Rdy Load/Cap Arches " \ - "Last Update " - hdr = hdr.format(longest_host=longest_host, hostname='Hostname') - if options.description: - hdr += "Description".ljust(51) - if options.comment: - hdr += "Comment".ljust(51) - if options.show_channels: - hdr += "Channels" - print(hdr) - print(len(hdr) * '-') - mask = "%%(name)-%ss %%(enabled)-3s %%(ready)-3s %%(task_load)4.1f/%%(capacity)-4.1f " \ - "%%(arches)-16s %%(update)-35s" % longest_host - if options.description: - mask += " %(description)-50s" - if options.comment: - mask += " %(comment)-50s" - if options.show_channels: - mask += " %(channels)s" - for host in hosts: - print(mask % host) - - diff --git a/cli/koji_cli/commands/cmd_list_notifications.py b/cli/koji_cli/commands/cmd_list_notifications.py deleted file mode 100644 index 0ae68ce..0000000 --- a/cli/koji_cli/commands/cmd_list_notifications.py +++ /dev/null @@ -1,86 +0,0 @@ -from __future__ import absolute_import, division - -from optparse import OptionParser - - - -from koji_cli.lib import ( - activate_session, - ensure_connection, - error, - get_usage_str -) - - -def anon_handle_list_notifications(goptions, session, args): - "[monitor] List user's notifications and blocks" - usage = "usage: %prog list-notifications [options]" - parser = OptionParser(usage=get_usage_str(usage)) - parser.add_option("--mine", action="store_true", help="Just print your notifications") - parser.add_option("--user", help="Only notifications for this user") - (options, args) = parser.parse_args(args) - - if len(args) != 0: - parser.error("This command takes no arguments") - if not options.mine and not options.user: - parser.error("Use --user or --mine.") - - if options.user: - ensure_connection(session, goptions) - user = session.getUser(options.user) - if not user: - error("No such user: %s" % options.user) - user_id = user['id'] - else: - activate_session(session, goptions) - user_id = None - - mask = "%(id)6s %(tag)-25s %(package)-25s %(email)-20s %(success)-12s" - headers = {'id': 'ID', - 'tag': 'Tag', - 'package': 'Package', - 'email': 'E-mail', - 'success': 'Success-only'} - head = mask % headers - notifications = session.getBuildNotifications(user_id) - if notifications: - print('Notifications') - print(head) - print('-' * len(head)) - for notification in notifications: - if notification['tag_id']: - notification['tag'] = session.getTag(notification['tag_id'])['name'] - else: - notification['tag'] = '*' - if notification['package_id']: - notification['package'] = session.getPackage(notification['package_id'])['name'] - else: - notification['package'] = '*' - notification['success'] = ['no', 'yes'][notification['success_only']] - print(mask % notification) - else: - print('No notifications') - - print('') - - mask = "%(id)6s %(tag)-25s %(package)-25s" - head = mask % headers - blocks = session.getBuildNotificationBlocks(user_id) - if blocks: - print('Notification blocks') - print(head) - print('-' * len(head)) - for notification in blocks: - if notification['tag_id']: - notification['tag'] = session.getTag(notification['tag_id'])['name'] - else: - notification['tag'] = '*' - if notification['package_id']: - notification['package'] = session.getPackage(notification['package_id'])['name'] - else: - notification['package'] = '*' - print(mask % notification) - else: - print('No notification blocks') - - diff --git a/cli/koji_cli/commands/cmd_list_permissions.py b/cli/koji_cli/commands/cmd_list_permissions.py deleted file mode 100644 index 4e8b6b2..0000000 --- a/cli/koji_cli/commands/cmd_list_permissions.py +++ /dev/null @@ -1,60 +0,0 @@ -from __future__ import absolute_import, division - -from optparse import OptionParser - - - -from koji_cli.lib import ( - activate_session, - error, - get_usage_str -) - - -def handle_list_permissions(goptions, session, args): - "[info] List user permissions" - usage = "usage: %prog list-permissions [options]" - parser = OptionParser(usage=get_usage_str(usage)) - parser.add_option("--user", help="List permissions for the given user") - parser.add_option("--mine", action="store_true", help="List your permissions") - parser.add_option("--quiet", action="store_true", default=goptions.quiet, - help="Do not print the header information") - (options, args) = parser.parse_args(args) - if len(args) > 0: - parser.error("This command takes no arguments") - activate_session(session, goptions) - perms = [] - if options.user: - user = session.getUser(options.user) - if not user: - error("No such user: %s" % options.user) - for p in session.getUserPerms(user['id']): - perms.append({'name': p}) - elif options.mine: - for p in session.getPerms(): - perms.append({'name': p}) - else: - for p in session.getAllPerms(): - perms.append({'name': p['name'], 'description': p['description']}) - if perms: - longest_perm = max([len(perm['name']) for perm in perms]) - perms = sorted(perms, key=lambda x: x['name']) - else: - longest_perm = 8 - if longest_perm < len('Permission name '): - longest_perm = len('Permission name ') - if not options.quiet: - hdr = '{permname:<{longest_perm}}' - hdr = hdr.format(longest_perm=longest_perm, permname='Permission name') - if perms and perms[0].get('description'): - hdr += " Description".ljust(53) - print(hdr) - print(len(hdr) * '-') - for perm in perms: - line = '{permname:<{longest_perm}}' - line = line.format(longest_perm=longest_perm, permname=perm['name']) - if perm.get('description'): - line += " %s" % perm['description'] - print(line) - - diff --git a/cli/koji_cli/commands/cmd_list_pkgs.py b/cli/koji_cli/commands/cmd_list_pkgs.py deleted file mode 100644 index 4cb3820..0000000 --- a/cli/koji_cli/commands/cmd_list_pkgs.py +++ /dev/null @@ -1,107 +0,0 @@ -from __future__ import absolute_import, division - -import time -from optparse import OptionParser - - -import koji - -from koji_cli.lib import ( - ensure_connection, - error, - get_usage_str -) - - -def anon_handle_list_pkgs(goptions, session, args): - "[info] Print the package listing for tag or for owner" - usage = "usage: %prog list-pkgs [options]" - parser = OptionParser(usage=get_usage_str(usage)) - parser.add_option("--owner", help="Specify owner") - parser.add_option("--tag", help="Specify tag") - parser.add_option("--package", help="Specify package") - parser.add_option("--quiet", action="store_true", default=goptions.quiet, - help="Do not print header information") - parser.add_option("--noinherit", action="store_true", help="Don't follow inheritance") - parser.add_option("--show-blocked", action="store_true", help="Show blocked packages") - parser.add_option("--show-dups", action="store_true", help="Show superseded owners") - parser.add_option("--event", type='int', metavar="EVENT#", help="query at event") - parser.add_option("--ts", type='int', metavar="TIMESTAMP", - help="query at last event before timestamp") - parser.add_option("--repo", type='int', metavar="REPO#", help="query at event for a repo") - (options, args) = parser.parse_args(args) - if len(args) != 0: - parser.error("This command takes no arguments") - ensure_connection(session, goptions) - opts = {} - if options.owner: - user = session.getUser(options.owner) - if user is None: - parser.error("No such user: %s" % options.owner) - opts['userID'] = user['id'] - if options.tag: - tag = session.getTag(options.tag) - if tag is None: - parser.error("No such tag: %s" % options.tag) - opts['tagID'] = tag['id'] - if options.package: - opts['pkgID'] = options.package - allpkgs = False - if not opts: - # no limiting clauses were specified - allpkgs = True - opts['inherited'] = not options.noinherit - # hiding dups only makes sense if we're querying a tag - if options.tag: - opts['with_dups'] = options.show_dups - else: - opts['with_dups'] = True - event = koji.util.eventFromOpts(session, options) - if event: - opts['event'] = event['id'] - event['timestr'] = time.asctime(time.localtime(event['ts'])) - print("Querying at event %(id)i (%(timestr)s)" % event) - - if not opts.get('tagID') and not opts.get('userID') and \ - not opts.get('pkgID'): - if opts.get('event'): - parser.error("--event and --ts makes sense only with --tag," - " --owner or --package") - if options.show_blocked: - parser.error("--show-blocked makes sense only with --tag," - " --owner or --package") - if options.show_blocked: - opts['with_blocked'] = options.show_blocked - - try: - data = session.listPackages(**opts) - except koji.ParameterError: - del opts['with_blocked'] - data = session.listPackages(**opts) - - if not data: - error("(no matching packages)") - if not options.quiet: - if allpkgs: - print("Package") - print('-' * 23) - else: - print("%-23s %-23s %-16s %-15s" % ('Package', 'Tag', 'Extra Arches', 'Owner')) - print("%s %s %s %s" % ('-' * 23, '-' * 23, '-' * 16, '-' * 15)) - for pkg in data: - if allpkgs: - print(pkg['package_name']) - else: - if not options.show_blocked and pkg.get('blocked', False): - continue - if 'tag_id' in pkg: - if pkg['extra_arches'] is None: - pkg['extra_arches'] = "" - fmt = "%(package_name)-23s %(tag_name)-23s %(extra_arches)-16s %(owner_name)-15s" - if pkg.get('blocked', False): - fmt += " [BLOCKED]" - else: - fmt = "%(package_name)s" - print(fmt % pkg) - - diff --git a/cli/koji_cli/commands/cmd_list_signed.py b/cli/koji_cli/commands/cmd_list_signed.py deleted file mode 100644 index 1a5fc75..0000000 --- a/cli/koji_cli/commands/cmd_list_signed.py +++ /dev/null @@ -1,106 +0,0 @@ -from __future__ import absolute_import, division - -import os -from optparse import SUPPRESS_HELP, OptionParser - - -import koji - -from koji_cli.lib import ( - activate_session, - get_usage_str -) - - -def handle_list_signed(goptions, session, args): - "[admin] List signed copies of rpms" - usage = "usage: %prog list-signed [options]" - description = "You must have local access to Koji's topdir filesystem." - parser = OptionParser(usage=get_usage_str(usage), description=description) - # Don't use local debug option, this one stays here for backward compatibility - # https://pagure.io/koji/issue/2084 - parser.add_option("--debug", action="store_true", default=goptions.debug, help=SUPPRESS_HELP) - parser.add_option("--key", help="Only list RPMs signed with this key") - parser.add_option("--build", help="Only list RPMs from this build") - parser.add_option("--rpm", help="Only list signed copies for this RPM") - parser.add_option("--tag", help="Only list RPMs within this tag") - (options, args) = parser.parse_args(args) - if not options.build and not options.tag and not options.rpm: - parser.error("At least one from --build, --rpm, --tag needs to be specified.") - activate_session(session, goptions) - qopts = {} - build_idx = {} - rpm_idx = {} - if options.key: - qopts['sigkey'] = options.key - - sigs = [] - if options.rpm: - rpm_info = options.rpm - try: - rpm_info = int(rpm_info) - except ValueError: - pass - rinfo = session.getRPM(rpm_info, strict=True) - rpm_idx[rinfo['id']] = rinfo - if rinfo.get('external_repo_id'): - parser.error("External rpm: %(name)s-%(version)s-%(release)s.%(arch)s@" - "%(external_repo_name)s" % rinfo) - sigs += session.queryRPMSigs(rpm_id=rinfo['id'], **qopts) - if options.build: - build = options.build - try: - build = int(build) - except ValueError: - pass - binfo = session.getBuild(build, strict=True) - build_idx[binfo['id']] = binfo - rpms = session.listRPMs(buildID=binfo['id']) - for rinfo in rpms: - rpm_idx[rinfo['id']] = rinfo - sigs += session.queryRPMSigs(rpm_id=rinfo['id'], **qopts) - if options.tag: - tag = options.tag - try: - tag = int(tag) - except ValueError: - pass - rpms, builds = session.listTaggedRPMS(tag, inherit=False, latest=False) - tagged = {} - for binfo in builds: - build_idx.setdefault(binfo['id'], binfo) - results = [] - # use batched multicall as there could be potentially a lot of results - # so we don't exhaust server resources - with session.multicall(batch=5000) as m: - for rinfo in rpms: - rpm_idx.setdefault(rinfo['id'], rinfo) - tagged[rinfo['id']] = 1 - results.append(m.queryRPMSigs(rpm_id=rinfo['id']), **qopts) - sigs += [x.result[0] for x in results] - - # Now figure out which sig entries actually have live copies - for sig in sigs: - rpm_id = sig['rpm_id'] - sigkey = sig['sigkey'] - if options.tag: - if tagged.get(rpm_id) is None: - continue - rinfo = rpm_idx.get(rpm_id) - if not rinfo: - rinfo = session.getRPM(rpm_id) - rpm_idx[rinfo['id']] = rinfo - binfo = build_idx.get(rinfo['build_id']) - if not binfo: - binfo = session.getBuild(rinfo['build_id']) - build_idx[binfo['id']] = binfo - binfo['name'] = binfo['package_name'] - builddir = koji.pathinfo.build(binfo) - signedpath = "%s/%s" % (builddir, koji.pathinfo.signed(rinfo, sigkey)) - if not os.path.exists(signedpath): - if goptions.debug: - print("No copy: %s" % signedpath) - continue - print(signedpath) - - diff --git a/cli/koji_cli/commands/cmd_list_tag_inheritance.py b/cli/koji_cli/commands/cmd_list_tag_inheritance.py deleted file mode 100644 index 5bf291e..0000000 --- a/cli/koji_cli/commands/cmd_list_tag_inheritance.py +++ /dev/null @@ -1,68 +0,0 @@ -from __future__ import absolute_import, division - -import sys -import textwrap -import time -from optparse import SUPPRESS_HELP, OptionParser - - -import koji - -from koji_cli.lib import ( - ensure_connection, - get_usage_str -) - - -def anon_handle_list_tag_inheritance(goptions, session, args): - "[info] Print the inheritance information for a tag" - usage = """\ - usage: %prog list-tag-inheritance [options] - - Prints tag inheritance with basic information about links. - Four flags could be seen in the output: - M - maxdepth - limits inheritance to n-levels - F - package filter (packages ignored for inheritance) - I - intransitive link - inheritance immediately stops here - N - noconfig - if tag is used in buildroot, its configuration values will not be used - - Exact values for maxdepth and package filter can be inquired by taginfo command. - """ - usage = textwrap.dedent(usage) - parser = OptionParser(usage=get_usage_str(usage)) - parser.add_option("--reverse", action="store_true", - help="Process tag's children instead of its parents") - parser.add_option("--stop", help=SUPPRESS_HELP) - parser.add_option("--jump", help=SUPPRESS_HELP) - parser.add_option("--event", type='int', metavar="EVENT#", help="query at event") - parser.add_option("--ts", type='int', metavar="TIMESTAMP", - help="query at last event before timestamp") - parser.add_option("--repo", type='int', metavar="REPO#", help="query at event for a repo") - (options, args) = parser.parse_args(args) - if len(args) != 1: - parser.error("This command takes exactly one argument: a tag name or ID") - for deprecated in ('stop', 'jump'): - if getattr(options, deprecated): - parser.error("--%s option has been removed in 1.26" % deprecated) - ensure_connection(session, goptions) - event = koji.util.eventFromOpts(session, options) - if event: - event['timestr'] = time.asctime(time.localtime(event['ts'])) - print("Querying at event %(id)i (%(timestr)s)" % event) - if event: - tag = session.getTag(args[0], event=event['id']) - else: - tag = session.getTag(args[0]) - if not tag: - parser.error("No such tag: %s" % args[0]) - - opts = {} - opts['reverse'] = options.reverse or False - if event: - opts['event'] = event['id'] - - sys.stdout.write(' %s (%i)\n' % (tag['name'], tag['id'])) - data = session.getFullInheritance(tag['id'], **opts) - _printInheritance(data, None, opts['reverse']) - - diff --git a/cli/koji_cli/commands/cmd_list_tagged.py b/cli/koji_cli/commands/cmd_list_tagged.py deleted file mode 100644 index aa70f9c..0000000 --- a/cli/koji_cli/commands/cmd_list_tagged.py +++ /dev/null @@ -1,130 +0,0 @@ -from __future__ import absolute_import, division - -import os -import time -from optparse import OptionParser - - -import koji - -from koji_cli.lib import ( - ensure_connection, - get_usage_str -) - - -def anon_handle_list_tagged(goptions, session, args): - "[info] List the builds or rpms in a tag" - usage = "usage: %prog list-tagged [options] []" - parser = OptionParser(usage=get_usage_str(usage)) - parser.add_option("--arch", action="append", default=[], help="List rpms for this arch") - parser.add_option("--rpms", action="store_true", help="Show rpms instead of builds") - parser.add_option("--inherit", action="store_true", help="Follow inheritance") - parser.add_option("--latest", action="store_true", help="Only show the latest builds/rpms") - parser.add_option("--latest-n", type='int', metavar="N", - help="Only show the latest N builds/rpms") - parser.add_option("--quiet", action="store_true", default=goptions.quiet, - help="Do not print the header information") - parser.add_option("--paths", action="store_true", help="Show the file paths") - parser.add_option("--sigs", action="store_true", help="Show signatures") - parser.add_option("--type", - help="Show builds of the given type only. " - "Currently supported types: maven, win, image") - parser.add_option("--event", type='int', metavar="EVENT#", help="query at event") - parser.add_option("--ts", type='int', metavar="TIMESTAMP", - help="query at last event before timestamp") - parser.add_option("--repo", type='int', metavar="REPO#", help="query at event for a repo") - (options, args) = parser.parse_args(args) - if len(args) == 0: - parser.error("A tag name must be specified") - elif len(args) > 2: - parser.error("Only one package name may be specified") - ensure_connection(session, goptions) - pathinfo = koji.PathInfo() - package = None - if len(args) > 1: - package = args[1] - tag = args[0] - opts = {} - for key in ('latest', 'inherit'): - opts[key] = getattr(options, key) - if options.latest_n is not None: - opts['latest'] = options.latest_n - if package: - opts['package'] = package - if options.arch: - options.rpms = True - opts['arch'] = options.arch - if options.sigs: - opts['rpmsigs'] = True - options.rpms = True - if options.type: - opts['type'] = options.type - event = koji.util.eventFromOpts(session, options) - event_id = None - if event: - opts['event'] = event['id'] - event_id = event['id'] - event['timestr'] = time.asctime(time.localtime(event['ts'])) - if not options.quiet: - print("Querying at event %(id)i (%(timestr)s)" % event) - - # check if tag exist(s|ed) - taginfo = session.getTag(tag, event=event_id) - if not taginfo: - parser.error("No such tag: %s" % tag) - - if options.rpms: - rpms, builds = session.listTaggedRPMS(tag, **opts) - data = rpms - if options.paths: - build_idx = dict([(b['id'], b) for b in builds]) - for rinfo in data: - build = build_idx[rinfo['build_id']] - builddir = pathinfo.build(build) - if options.sigs: - sigkey = rinfo['sigkey'] - signedpath = os.path.join(builddir, pathinfo.signed(rinfo, sigkey)) - if os.path.exists(signedpath): - rinfo['path'] = signedpath - else: - rinfo['path'] = os.path.join(builddir, pathinfo.rpm(rinfo)) - fmt = "%(path)s" - data = [x for x in data if 'path' in x] - else: - fmt = "%(name)s-%(version)s-%(release)s.%(arch)s" - if options.sigs: - fmt = "%(sigkey)s " + fmt - else: - data = session.listTagged(tag, **opts) - if options.paths: - if options.type == 'maven': - for x in data: - x['path'] = pathinfo.mavenbuild(x) - fmt = "%(path)-40s %(tag_name)-20s %(maven_group_id)-20s " \ - "%(maven_artifact_id)-20s %(owner_name)s" - else: - for x in data: - x['path'] = pathinfo.build(x) - fmt = "%(path)-40s %(tag_name)-20s %(owner_name)s" - else: - if options.type == 'maven': - fmt = "%(nvr)-40s %(tag_name)-20s %(maven_group_id)-20s " \ - "%(maven_artifact_id)-20s %(owner_name)s" - else: - fmt = "%(nvr)-40s %(tag_name)-20s %(owner_name)s" - if not options.quiet: - if options.type == 'maven': - print("%-40s %-20s %-20s %-20s %s" % - ("Build", "Tag", "Group Id", "Artifact Id", "Built by")) - print("%s %s %s %s %s" % - ("-" * 40, "-" * 20, "-" * 20, "-" * 20, "-" * 16)) - else: - print("%-40s %-20s %s" % ("Build", "Tag", "Built by")) - print("%s %s %s" % ("-" * 40, "-" * 20, "-" * 16)) - - output = sorted([fmt % x for x in data]) - for line in output: - print(line) - - diff --git a/cli/koji_cli/commands/cmd_list_tags.py b/cli/koji_cli/commands/cmd_list_tags.py deleted file mode 100644 index d61a417..0000000 --- a/cli/koji_cli/commands/cmd_list_tags.py +++ /dev/null @@ -1,328 +0,0 @@ -from __future__ import absolute_import, division - -import itertools -import pprint -import sys -import time -from datetime import datetime -from dateutil.tz import tzutc -from optparse import OptionParser - - -import koji -from koji.util import to_list - -from koji_cli.lib import ( - ensure_connection, - get_usage_str, - warn -) - - -def anon_handle_list_tags(goptions, session, args): - "[info] Print the list of tags" - usage = "usage: %prog list-tags [options] [pattern]" - parser = OptionParser(usage=get_usage_str(usage)) - parser.add_option("--show-id", action="store_true", help="Show tag ids") - parser.add_option("--verbose", action="store_true", help="Show more information") - parser.add_option("--unlocked", action="store_true", help="Only show unlocked tags") - parser.add_option("--build", help="Show tags associated with a build") - parser.add_option("--package", help="Show tags associated with a package") - (options, patterns) = parser.parse_args(args) - ensure_connection(session, goptions) - - pkginfo = {} - buildinfo = {} - - if options.package: - pkginfo = session.getPackage(options.package) - if not pkginfo: - parser.error("No such package: %s" % options.package) - - if options.build: - buildinfo = session.getBuild(options.build) - if not buildinfo: - parser.error("No such build: %s" % options.build) - - if not patterns: - # list everything if no pattern is supplied - tags = session.listTags(build=buildinfo.get('id', None), - package=pkginfo.get('id', None)) - else: - # The hub may not support the pattern option. We try with that first - # and fall back to the old way. - fallback = False - try: - tags = [] - with session.multicall(strict=True) as m: - for pattern in patterns: - tags.append(m.listTags(build=buildinfo.get('id', None), - package=pkginfo.get('id', None), - pattern=pattern)) - tags = list(itertools.chain(*[t.result for t in tags])) - except koji.ParameterError: - fallback = True - if fallback: - # without the pattern option, we have to filter client side - tags = session.listTags(buildinfo.get('id', None), pkginfo.get('id', None)) - tags = [t for t in tags if koji.util.multi_fnmatch(t['name'], patterns)] - - tags.sort(key=lambda x: x['name']) - # if options.verbose: - # fmt = "%(name)s [%(id)i] %(perm)s %(locked)s %(arches)s" - if options.show_id: - fmt = "%(name)s [%(id)i]" - else: - fmt = "%(name)s" - for tag in tags: - if options.unlocked: - if tag['locked'] or tag['perm']: - continue - if not options.verbose: - print(fmt % tag) - else: - sys.stdout.write(fmt % tag) - if tag['locked']: - sys.stdout.write(' [LOCKED]') - if tag['perm']: - sys.stdout.write(' [%(perm)s perm required]' % tag) - print('') - - -def _print_histline(entry, **kwargs): - options = kwargs['options'] - event_id, table, create, x = entry - who = None - edit = x.get('.related') - if edit: - del x['.related'] - bad_edit = None - if len(edit) != 1: - bad_edit = "%i elements" % (len(edit) + 1) - other = edit[0] - # check edit for sanity - if create or not other[2]: - bad_edit = "out of order" - if event_id != other[0]: - bad_edit = "non-matching" - if bad_edit: - warn("Unusual edit at event %i in table %s (%s)" % (event_id, table, bad_edit)) - # we'll simply treat them as separate events - pprint.pprint(entry) - pprint.pprint(edit) - _print_histline(entry, **kwargs) - for data in edit: - _print_histline(entry, **kwargs) - return - if create: - ts = x['create_ts'] - if 'creator_name' in x: - who = "by %(creator_name)s" - else: - ts = x['revoke_ts'] - if 'revoker_name' in x: - who = "by %(revoker_name)s" - if table == 'tag_listing': - if edit: - fmt = "%(name)s-%(version)s-%(release)s re-tagged into %(tag.name)s" - elif create: - fmt = "%(name)s-%(version)s-%(release)s tagged into %(tag.name)s" - else: - fmt = "%(name)s-%(version)s-%(release)s untagged from %(tag.name)s" - elif table == 'user_perms': - if edit: - fmt = "permission %(permission.name)s re-granted to %(user.name)s" - elif create: - fmt = "permission %(permission.name)s granted to %(user.name)s" - else: - fmt = "permission %(permission.name)s revoked for %(user.name)s" - elif table == 'user_groups': - if edit: - fmt = "user %(user.name)s re-added to group %(group.name)s" - elif create: - fmt = "user %(user.name)s added to group %(group.name)s" - else: - fmt = "user %(user.name)s removed from group %(group.name)s" - elif table == 'cg_users': - if edit: - fmt = "user %(user.name)s re-added to content generator %(content_generator.name)s" - elif create: - fmt = "user %(user.name)s added to content generator %(content_generator.name)s" - else: - fmt = "user %(user.name)s removed from content generator %(content_generator.name)s" - elif table == 'tag_packages': - if edit: - fmt = "package list entry for %(package.name)s in %(tag.name)s updated" - elif create: - fmt = "package list entry created: %(package.name)s in %(tag.name)s" - else: - fmt = "package list entry revoked: %(package.name)s in %(tag.name)s" - elif table == 'tag_package_owners': - if edit: - fmt = "package owner changed for %(package.name)s in %(tag.name)s" - elif create: - fmt = "package owner %(owner.name)s set for %(package.name)s in %(tag.name)s" - else: - fmt = "package owner %(owner.name)s revoked for %(package.name)s in %(tag.name)s" - elif table == 'tag_inheritance': - if edit: - fmt = "inheritance line %(tag.name)s->%(parent.name)s updated" - elif create: - fmt = "inheritance line %(tag.name)s->%(parent.name)s added" - else: - fmt = "inheritance line %(tag.name)s->%(parent.name)s removed" - elif table == 'tag_config': - if edit: - fmt = "tag configuration for %(tag.name)s altered" - elif create: - fmt = "new tag: %(tag.name)s" - else: - fmt = "tag deleted: %(tag.name)s" - elif table == 'tag_extra': - if edit: - fmt = "tag option %(key)s for tag %(tag.name)s altered" - elif create: - fmt = "added tag option %(key)s for tag %(tag.name)s" - else: - fmt = "tag option %(key)s removed for %(tag.name)s" - elif table == 'host_config': - if edit: - fmt = "host configuration for %(host.name)s altered" - elif create: - fmt = "new host: %(host.name)s" - else: - fmt = "host deleted: %(host.name)s" - elif table == 'host_channels': - if create: - fmt = "host %(host.name)s added to channel %(channels.name)s" - else: - fmt = "host %(host.name)s removed from channel %(channels.name)s" - elif table == 'build_target_config': - if edit: - fmt = "build target configuration for %(build_target.name)s updated" - elif create: - fmt = "new build target: %(build_target.name)s" - else: - fmt = "build target deleted: %(build_target.name)s" - elif table == 'external_repo_config': - if edit: - fmt = "external repo configuration for %(external_repo.name)s altered" - elif create: - fmt = "new external repo: %(external_repo.name)s" - else: - fmt = "external repo deleted: %(external_repo.name)s" - elif table == 'tag_external_repos': - if edit: - fmt = "external repo entry for %(external_repo.name)s in tag %(tag.name)s updated" - elif create: - fmt = "external repo entry for %(external_repo.name)s added to tag %(tag.name)s" - else: - fmt = "external repo entry for %(external_repo.name)s removed from tag %(tag.name)s" - elif table == 'group_config': - if edit: - fmt = "group %(group.name)s configuration for tag %(tag.name)s updated" - elif create: - fmt = "group %(group.name)s added to tag %(tag.name)s" - else: - fmt = "group %(group.name)s removed from tag %(tag.name)s" - elif table == 'group_req_listing': - if edit: - fmt = "group dependency %(group.name)s->%(req.name)s updated in tag %(tag.name)s" - elif create: - fmt = "group dependency %(group.name)s->%(req.name)s added in tag %(tag.name)s" - else: - fmt = "group dependency %(group.name)s->%(req.name)s dropped from tag %(tag.name)s" - elif table == 'group_package_listing': - if edit: - fmt = "package entry %(package)s in group %(group.name)s, tag %(tag.name)s updated" - elif create: - fmt = "package %(package)s added to group %(group.name)s in tag %(tag.name)s" - else: - fmt = "package %(package)s removed from group %(group.name)s in tag %(tag.name)s" - else: - if edit: - fmt = "%s entry updated" % table - elif create: - fmt = "%s entry created" % table - else: - fmt = "%s entry revoked" % table - if options.utc: - time_str = time.asctime(datetime.fromtimestamp(ts, tzutc()).timetuple()) - else: - time_str = time.asctime(time.localtime(ts)) - - parts = [time_str, fmt % x] - if options.events or options.verbose: - parts.insert(1, "(eid %i)" % event_id) - if who: - parts.append(who % x) - if create and x['active']: - parts.append("[still active]") - print(' '.join(parts)) - hidden_fields = ['active', 'create_event', 'revoke_event', 'creator_id', 'revoker_id', - 'creator_name', 'revoker_name', 'create_ts', 'revoke_ts'] - - def get_nkey(key): - if key == 'perm_id': - return 'permission.name' - elif key.endswith('_id'): - return '%s.name' % key[:-3] - else: - return '%s.name' % key - if edit: - keys = sorted(to_list(x.keys())) - y = other[-1] - for key in keys: - if key in hidden_fields: - continue - if x[key] == y[key]: - continue - if key[0] == '_': - continue - nkey = get_nkey(key) - if nkey in x and nkey in y: - continue - print(" %s: %s -> %s" % (key, x[key], y[key])) - elif create and options.verbose and table != 'tag_listing': - keys = sorted(to_list(x.keys())) - # the table keys have already been represented in the base format string - also_hidden = list(_table_keys[table]) - also_hidden.extend([get_nkey(k) for k in also_hidden]) - for key in keys: - if key in hidden_fields or key in also_hidden: - continue - nkey = get_nkey(key) - if nkey in x: - continue - if key[0] == '_': - continue - if x.get('blocked') and key != 'blocked': - continue - if key.endswith('.name'): - dkey = key[:-5] - else: - dkey = key - print(" %s: %s" % (dkey, x[key])) - - -_table_keys = { - 'user_perms': ['user_id', 'perm_id'], - 'user_groups': ['user_id', 'group_id'], - 'cg_users': ['user_id', 'cg_id'], - 'tag_inheritance': ['tag_id', 'parent_id'], - 'tag_config': ['tag_id'], - 'tag_extra': ['tag_id', 'key'], - 'build_target_config': ['build_target_id'], - 'external_repo_config': ['external_repo_id'], - 'host_config': ['host_id'], - 'host_channels': ['host_id', 'channel_id'], - 'tag_external_repos': ['tag_id', 'external_repo_id'], - 'tag_listing': ['build_id', 'tag_id'], - 'tag_packages': ['package_id', 'tag_id'], - 'tag_package_owners': ['package_id', 'tag_id'], - 'group_config': ['group_id', 'tag_id'], - 'group_req_listing': ['group_id', 'tag_id', 'req_id'], - 'group_package_listing': ['group_id', 'tag_id', 'package'], -} - - diff --git a/cli/koji_cli/commands/cmd_list_targets.py b/cli/koji_cli/commands/cmd_list_targets.py deleted file mode 100644 index 296d8e8..0000000 --- a/cli/koji_cli/commands/cmd_list_targets.py +++ /dev/null @@ -1,87 +0,0 @@ -from __future__ import absolute_import, division - -import sys -from optparse import OptionParser - - - -from koji_cli.lib import ( - ensure_connection, - format_inheritance_flags, - get_usage_str -) - - -def anon_handle_list_targets(goptions, session, args): - "[info] List the build targets" - usage = "usage: %prog list-targets [options]" - parser = OptionParser(usage=get_usage_str(usage)) - parser.add_option("--name", help="Specify the build target name") - parser.add_option("--quiet", action="store_true", default=goptions.quiet, - help="Do not print the header information") - (options, args) = parser.parse_args(args) - - if len(args) != 0: - parser.error("This command takes no arguments") - ensure_connection(session, goptions) - - targets = session.getBuildTargets(options.name) - if len(targets) == 0: - if options.name: - parser.error('No such build target: %s' % options.name) - else: - parser.error('No targets were found') - - fmt = "%(name)-30s %(build_tag_name)-30s %(dest_tag_name)-30s" - if not options.quiet: - print("%-30s %-30s %-30s" % ('Name', 'Buildroot', 'Destination')) - print("-" * 93) - tmp_list = sorted([(x['name'], x) for x in targets]) - targets = [x[1] for x in tmp_list] - for target in targets: - print(fmt % target) - # pprint.pprint(session.getBuildTargets()) - - -def _printInheritance(tags, sibdepths=None, reverse=False): - if len(tags) == 0: - return - if sibdepths is None: - sibdepths = [] - currtag = tags[0] - tags = tags[1:] - if reverse: - siblings = len([tag for tag in tags if tag['parent_id'] == currtag['parent_id']]) - else: - siblings = len([tag for tag in tags if tag['child_id'] == currtag['child_id']]) - - sys.stdout.write(format_inheritance_flags(currtag)) - outdepth = 0 - for depth in sibdepths: - if depth < currtag['currdepth']: - outspacing = depth - outdepth - sys.stdout.write(' ' * (outspacing * 3 - 1)) - sys.stdout.write(_printable_unicode(u'\u2502')) - outdepth = depth - - sys.stdout.write(' ' * ((currtag['currdepth'] - outdepth) * 3 - 1)) - if siblings: - sys.stdout.write(_printable_unicode(u'\u251c')) - else: - sys.stdout.write(_printable_unicode(u'\u2514')) - sys.stdout.write(_printable_unicode(u'\u2500')) - if reverse: - sys.stdout.write('%(name)s (%(tag_id)i)\n' % currtag) - else: - sys.stdout.write('%(name)s (%(parent_id)i)\n' % currtag) - - if siblings: - if len(sibdepths) == 0 or sibdepths[-1] != currtag['currdepth']: - sibdepths.append(currtag['currdepth']) - else: - if len(sibdepths) > 0 and sibdepths[-1] == currtag['currdepth']: - sibdepths.pop() - - _printInheritance(tags, sibdepths, reverse) - - diff --git a/cli/koji_cli/commands/cmd_list_tasks.py b/cli/koji_cli/commands/cmd_list_tasks.py deleted file mode 100644 index 771318d..0000000 --- a/cli/koji_cli/commands/cmd_list_tasks.py +++ /dev/null @@ -1,55 +0,0 @@ -from __future__ import absolute_import, division - -from optparse import OptionParser - - - -from koji_cli.lib import ( - TimeOption, - _list_tasks, - activate_session, - get_usage_str, - print_task_headers, - print_task_recurse -) - - -def handle_list_tasks(goptions, session, args): - "[info] Print the list of tasks" - usage = "usage: %prog list-tasks [options]" - parser = OptionParser(usage=get_usage_str(usage), option_class=TimeOption) - parser.add_option("--mine", action="store_true", help="Just print your tasks") - parser.add_option("--user", help="Only tasks for this user") - parser.add_option("--arch", help="Only tasks for this architecture") - parser.add_option("--method", help="Only tasks of this method") - parser.add_option("--channel", help="Only tasks in this channel") - parser.add_option("--host", help="Only tasks for this host") - parser.add_option("--quiet", action="store_true", default=goptions.quiet, - help="Do not display the column headers") - parser.add_option("--before", type="time", - help="List tasks completed before this time, " + TimeOption.get_help()) - parser.add_option("--after", type="time", - help="List tasks completed after this time (same format as for --before") - parser.add_option("--all", action="store_true", - help="List also finished tasks (valid only with --after)") - (options, args) = parser.parse_args(args) - if len(args) != 0: - parser.error("This command takes no arguments") - - if options.all and not options.after: - parser.error("--all must be used with --after") - - activate_session(session, goptions) - tasklist = _list_tasks(options, session) - if not tasklist: - print("(no tasks)") - return - if not options.quiet: - print_task_headers() - for t in tasklist: - if t.get('sub'): - # this subtask will appear under another task - continue - print_task_recurse(t) - - diff --git a/cli/koji_cli/commands/cmd_list_untagged.py b/cli/koji_cli/commands/cmd_list_untagged.py deleted file mode 100644 index ef55921..0000000 --- a/cli/koji_cli/commands/cmd_list_untagged.py +++ /dev/null @@ -1,77 +0,0 @@ -from __future__ import absolute_import, division - -from optparse import OptionParser - - -import koji - -from koji_cli.lib import ( - ensure_connection, - error, - get_usage_str -) - - -def anon_handle_list_untagged(goptions, session, args): - "[info] List untagged builds" - usage = "usage: %prog list-untagged [options] []" - parser = OptionParser(usage=get_usage_str(usage)) - parser.add_option("--paths", action="store_true", help="Show the file paths") - parser.add_option("--show-references", action="store_true", help="Show build references") - (options, args) = parser.parse_args(args) - if len(args) > 1: - parser.error("Only one package name may be specified") - ensure_connection(session, goptions) - package = None - if len(args) > 0: - package = args[0] - opts = {} - if package: - package_id = session.getPackageID(package) - if package_id is None: - error("No such package: %s" % package) - opts['name'] = package - pathinfo = koji.PathInfo() - - data = session.untaggedBuilds(**opts) - if options.show_references: - print("(Showing build references)") - references = {} - with session.multicall(strict=True, batch=10000) as m: - for build in data: - references[build['id']] = m.buildReferences(build['id']) - - for build in data: - refs = references[build['id']].result - r = [] - if refs.get('rpms'): - r.append("rpms: %s" % refs['rpms']) - if refs.get('component_of'): - r.append("images/archives: %s" % refs['component_of']) - if refs.get('archives'): - r.append("archives buildroots: %s" % refs['archives']) - build['refs'] = ', '.join(r) - if options.paths: - for x in data: - x['path'] = pathinfo.build(x) - fmt = "%(path)s" - else: - fmt = "%(name)s-%(version)s-%(release)s" - if options.show_references: - fmt = fmt + " %(refs)s" - output = sorted([fmt % x for x in data]) - for line in output: - print(line) - - -def print_group_list_req_group(group): - print(" @%(name)s [%(tag_name)s]" % group) - - -def print_group_list_req_package(pkg): - fmt = " %(package)s: %(basearchonly)s, %(type)s [%(tag_name)s]" - if pkg['blocked']: - fmt += " [BLOCKED]" - print(fmt % pkg) - - diff --git a/cli/koji_cli/commands/cmd_list_volumes.py b/cli/koji_cli/commands/cmd_list_volumes.py deleted file mode 100644 index 03405bf..0000000 --- a/cli/koji_cli/commands/cmd_list_volumes.py +++ /dev/null @@ -1,22 +0,0 @@ -from __future__ import absolute_import, division - -from optparse import OptionParser - - - -from koji_cli.lib import ( - ensure_connection, - get_usage_str -) - - -def anon_handle_list_volumes(goptions, session, args): - "[info] List storage volumes" - usage = "usage: %prog list-volumes" - parser = OptionParser(usage=get_usage_str(usage)) - (options, args) = parser.parse_args(args) - ensure_connection(session, goptions) - for volinfo in session.listVolumes(): - print(volinfo['name']) - - diff --git a/cli/koji_cli/commands/cmd_lock_tag.py b/cli/koji_cli/commands/cmd_lock_tag.py deleted file mode 100644 index e871a99..0000000 --- a/cli/koji_cli/commands/cmd_lock_tag.py +++ /dev/null @@ -1,62 +0,0 @@ -from __future__ import absolute_import, division - -import fnmatch -from optparse import OptionParser - - - -from koji_cli.lib import ( - activate_session, - get_usage_str -) - - -def handle_lock_tag(goptions, session, args): - "[admin] Lock a tag" - usage = "usage: %prog lock-tag [options] [ ...]" - parser = OptionParser(usage=get_usage_str(usage)) - parser.add_option("--perm", help="Specify permission requirement") - parser.add_option("--glob", action="store_true", help="Treat args as glob patterns") - parser.add_option("--master", action="store_true", help="Lock the master lock") - parser.add_option("-n", "--test", action="store_true", help="Test mode") - (options, args) = parser.parse_args(args) - if len(args) < 1: - parser.error("Please specify a tag") - activate_session(session, goptions) - pdata = session.getAllPerms() - perm_ids = dict([(p['name'], p['id']) for p in pdata]) - perm = options.perm - if perm is None: - perm = 'admin' - perm_id = perm_ids[perm] - if options.glob: - selected = [] - for tag in session.listTags(): - for pattern in args: - if fnmatch.fnmatch(tag['name'], pattern): - selected.append(tag) - break - if not selected: - print("No tags matched") - else: - selected = [session.getTag(name, strict=True) for name in args] - for tag in selected: - if options.master: - # set the master lock - if tag['locked']: - print("Tag %s: master lock already set" % tag['name']) - continue - elif options.test: - print("Would have set master lock for: %s" % tag['name']) - continue - session.editTag2(tag['id'], locked=True) - else: - if tag['perm_id'] == perm_id: - print("Tag %s: %s permission already required" % (tag['name'], perm)) - continue - elif options.test: - print("Would have set permission requirement %s for tag %s" % (perm, tag['name'])) - continue - session.editTag2(tag['id'], perm_id=perm_id) - - diff --git a/cli/koji_cli/commands/cmd_make_task.py b/cli/koji_cli/commands/cmd_make_task.py deleted file mode 100644 index 0d27182..0000000 --- a/cli/koji_cli/commands/cmd_make_task.py +++ /dev/null @@ -1,46 +0,0 @@ -from __future__ import absolute_import, division - -from optparse import OptionParser - -from six.moves import map - - -from koji_cli.lib import ( - _running_in_bg, - activate_session, - arg_filter, - get_usage_str, - watch_tasks -) - - -def handle_make_task(goptions, session, args): - "[admin] Create an arbitrary task" - usage = "usage: %prog make-task [options] [ ...]" - parser = OptionParser(usage=get_usage_str(usage)) - parser.add_option("--channel", help="set channel") - parser.add_option("--priority", help="set priority") - parser.add_option("--watch", action="store_true", help="watch the task") - parser.add_option("--arch", help="set arch") - (options, args) = parser.parse_args(args) - if len(args) < 1: - parser.error("Please specify task method at least") - - activate_session(session, goptions) - taskopts = {} - for key in ('channel', 'priority', 'arch'): - value = getattr(options, key, None) - if value is not None: - taskopts[key] = value - task_id = session.makeTask(method=args[0], - arglist=list(map(arg_filter, args[1:])), - **taskopts) - print("Created task id %d" % task_id) - if _running_in_bg() or not options.watch: - return - else: - session.logout() - return watch_tasks(session, [task_id], quiet=goptions.quiet, - poll_interval=goptions.poll_interval, topurl=goptions.topurl) - - diff --git a/cli/koji_cli/commands/cmd_maven_build.py b/cli/koji_cli/commands/cmd_maven_build.py deleted file mode 100644 index 2b3183f..0000000 --- a/cli/koji_cli/commands/cmd_maven_build.py +++ /dev/null @@ -1,104 +0,0 @@ -from __future__ import absolute_import, division - -from optparse import OptionParser - - -import koji -from koji.util import to_list - -from koji_cli.lib import ( - _running_in_bg, - activate_session, - get_usage_str, - watch_tasks -) - - -def handle_maven_build(options, session, args): - "[build] Build a Maven package from source" - usage = "usage: %prog maven-build [options] " - usage += "\n %prog maven-build --ini=CONFIG... [options] " - parser = OptionParser(usage=get_usage_str(usage)) - parser.add_option("--patches", action="store", metavar="URL", - help="SCM URL of a directory containing patches to apply to the sources " - "before building") - parser.add_option("-G", "--goal", action="append", dest="goals", metavar="GOAL", default=[], - help="Additional goal to run before \"deploy\"") - parser.add_option("-P", "--profile", action="append", dest="profiles", metavar="PROFILE", - default=[], help="Enable a profile for the Maven build") - parser.add_option("-D", "--property", action="append", dest="properties", metavar="NAME=VALUE", - default=[], help="Pass a system property to the Maven build") - parser.add_option("-E", "--env", action="append", dest="envs", metavar="NAME=VALUE", - default=[], help="Set an environment variable") - parser.add_option("-p", "--package", action="append", dest="packages", metavar="PACKAGE", - default=[], help="Install an additional package into the buildroot") - parser.add_option("-J", "--jvm-option", action="append", dest="jvm_options", metavar="OPTION", - default=[], help="Pass a command-line option to the JVM") - parser.add_option("-M", "--maven-option", action="append", dest="maven_options", - metavar="OPTION", default=[], help="Pass a command-line option to Maven") - parser.add_option("--ini", action="append", dest="inis", metavar="CONFIG", default=[], - help="Pass build parameters via a .ini file") - parser.add_option("-s", "--section", help="Get build parameters from this section of the .ini") - parser.add_option("--debug", action="store_true", help="Run Maven build in debug mode") - parser.add_option("--specfile", action="store", metavar="URL", - help="SCM URL of a spec file fragment to use to generate wrapper RPMs") - parser.add_option("--skip-tag", action="store_true", help="Do not attempt to tag package") - parser.add_option("--scratch", action="store_true", help="Perform a scratch build") - parser.add_option("--wait", action="store_true", - help="Wait on build, even if running in the background") - parser.add_option("--nowait", action="store_false", dest="wait", help="Don't wait on build") - parser.add_option("--quiet", action="store_true", - help="Do not print the task information", default=options.quiet) - parser.add_option("--background", action="store_true", - help="Run the build at a lower priority") - (build_opts, args) = parser.parse_args(args) - if build_opts.inis: - if len(args) != 1: - parser.error("Exactly one argument (a build target) is required") - else: - if len(args) != 2: - parser.error("Exactly two arguments (a build target and a SCM URL) are required") - activate_session(session, options) - target = args[0] - build_target = session.getBuildTarget(target) - if not build_target: - parser.error("No such build target: %s" % target) - dest_tag = session.getTag(build_target['dest_tag']) - if not dest_tag: - parser.error("No such destination tag: %s" % build_target['dest_tag_name']) - if dest_tag['locked'] and not build_opts.scratch: - parser.error("Destination tag %s is locked" % dest_tag['name']) - if build_opts.inis: - try: - params = koji.util.parse_maven_param(build_opts.inis, scratch=build_opts.scratch, - section=build_opts.section) - except ValueError as e: - parser.error(e.args[0]) - opts = to_list(params.values())[0] - if opts.pop('type', 'maven') != 'maven': - parser.error("Section %s does not contain a maven-build config" % - to_list(params.keys())[0]) - source = opts.pop('scmurl') - else: - source = args[1] - opts = koji.util.maven_opts(build_opts, scratch=build_opts.scratch) - if '://' not in source: - parser.error("No such SCM URL: %s" % source) - if build_opts.debug: - opts.setdefault('maven_options', []).append('--debug') - if build_opts.skip_tag: - opts['skip_tag'] = True - priority = None - if build_opts.background: - # relative to koji.PRIO_DEFAULT - priority = 5 - task_id = session.mavenBuild(source, target, opts, priority=priority) - if not build_opts.quiet: - print("Created task: %d" % task_id) - print("Task info: %s/taskinfo?taskID=%s" % (options.weburl, task_id)) - if build_opts.wait or (build_opts.wait is None and not _running_in_bg()): - session.logout() - return watch_tasks(session, [task_id], quiet=build_opts.quiet, - poll_interval=options.poll_interval, topurl=options.topurl) - - diff --git a/cli/koji_cli/commands/cmd_maven_chain.py b/cli/koji_cli/commands/cmd_maven_chain.py deleted file mode 100644 index d1f9666..0000000 --- a/cli/koji_cli/commands/cmd_maven_chain.py +++ /dev/null @@ -1,62 +0,0 @@ -from __future__ import absolute_import, division - -from optparse import OptionParser - - -import koji - -from koji_cli.lib import ( - _running_in_bg, - activate_session, - get_usage_str, - watch_tasks -) - - -def handle_maven_chain(options, session, args): - "[build] Run a set of Maven builds in dependency order" - usage = "usage: %prog maven-chain [options] [ ...]" - parser = OptionParser(usage=get_usage_str(usage)) - parser.add_option("--skip-tag", action="store_true", help="Do not attempt to tag builds") - parser.add_option("--scratch", action="store_true", help="Perform scratch builds") - parser.add_option("--debug", action="store_true", help="Run Maven build in debug mode") - parser.add_option("--force", action="store_true", help="Force rebuilds of all packages") - parser.add_option("--wait", action="store_true", - help="Wait on build, even if running in the background") - parser.add_option("--nowait", action="store_false", dest="wait", help="Don't wait on build") - parser.add_option("--background", action="store_true", - help="Run the build at a lower priority") - (build_opts, args) = parser.parse_args(args) - if len(args) < 2: - parser.error("Two arguments (a build target and a config file) are required") - activate_session(session, options) - target = args[0] - build_target = session.getBuildTarget(target) - if not build_target: - parser.error("No such build target: %s" % target) - dest_tag = session.getTag(build_target['dest_tag']) - if not dest_tag: - parser.error("No such destination tag: %s" % build_target['dest_tag_name']) - if dest_tag['locked'] and not build_opts.scratch: - parser.error("Destination tag %s is locked" % dest_tag['name']) - opts = {} - for key in ('skip_tag', 'scratch', 'debug', 'force'): - val = getattr(build_opts, key) - if val: - opts[key] = val - try: - builds = koji.util.parse_maven_chain(args[1:], scratch=opts.get('scratch')) - except ValueError as e: - parser.error(e.args[0]) - priority = None - if build_opts.background: - priority = 5 - task_id = session.chainMaven(builds, target, opts, priority=priority) - print("Created task: %d" % task_id) - print("Task info: %s/taskinfo?taskID=%s" % (options.weburl, task_id)) - if build_opts.wait or (build_opts.wait is None and not _running_in_bg()): - session.logout() - return watch_tasks(session, [task_id], quiet=options.quiet, - poll_interval=options.poll_interval, topurl=options.topurl) - - diff --git a/cli/koji_cli/commands/cmd_mock_config.py b/cli/koji_cli/commands/cmd_mock_config.py deleted file mode 100644 index 8b276bf..0000000 --- a/cli/koji_cli/commands/cmd_mock_config.py +++ /dev/null @@ -1,168 +0,0 @@ -from __future__ import absolute_import, division - -from optparse import OptionParser - - -import koji - -from koji_cli.lib import ( - ensure_connection, - error, - get_usage_str, - warn -) - - -def anon_handle_mock_config(goptions, session, args): - "[info] Create a mock config" - usage = "usage: %prog mock-config [options]" - parser = OptionParser(usage=get_usage_str(usage)) - parser.add_option("-a", "--arch", help="Specify the arch") - parser.add_option("-n", "--name", help="Specify the name for the buildroot") - parser.add_option("--tag", help="Create a mock config for a tag") - parser.add_option("--target", help="Create a mock config for a build target") - parser.add_option("--task", help="Duplicate the mock config of a previous task") - parser.add_option("--latest", action="store_true", help="use the latest redirect url") - parser.add_option("--buildroot", - help="Duplicate the mock config for the specified buildroot id") - parser.add_option("--mockdir", default="/var/lib/mock", metavar="DIR", help="Specify mockdir") - parser.add_option("--topdir", metavar="DIR", - help="Specify topdir, topdir tops the topurl") - parser.add_option("--topurl", metavar="URL", - help="URL under which Koji files are accessible, " - "when topdir is specified, topdir tops the topurl") - parser.add_option("--distribution", default="Koji Testing", - help="Change the distribution macro") - parser.add_option("--yum-proxy", help="Specify a yum proxy") - parser.add_option("-o", metavar="FILE", dest="ofile", help="Output to a file") - (options, args) = parser.parse_args(args) - ensure_connection(session, goptions) - if args: - # for historical reasons, we also accept buildroot name as first arg - if not options.name: - options.name = args[0] - else: - parser.error("Name already specified via option") - arch = None - opts = {} - for k in ('topdir', 'topurl', 'distribution', 'mockdir', 'yum_proxy'): - if hasattr(options, k): - if getattr(options, k) is not None: - opts[k] = getattr(options, k) - if opts.get('topdir') and opts.get('topurl'): - del opts['topurl'] - if not opts.get('topdir') and not opts.get('topurl'): - opts['topurl'] = goptions.topurl - if options.buildroot: - try: - br_id = int(options.buildroot) - except ValueError: - parser.error("Buildroot id must be an integer") - brootinfo = session.getBuildroot(br_id) - if brootinfo is None: - error("No such buildroot: %r" % br_id) - if options.latest: - opts['repoid'] = 'latest' - else: - opts['repoid'] = brootinfo['repo_id'] - opts['tag_name'] = brootinfo['tag_name'] - arch = brootinfo['arch'] - elif options.task: - try: - task_id = int(options.task) - except ValueError: - parser.error("Task id must be an integer") - broots = session.listBuildroots(taskID=task_id) - if not broots: - error("No buildroots for task %s (or no such task)" % options.task) - if len(broots) > 1: - print("Multiple buildroots found: %s" % [br['id'] for br in broots]) - brootinfo = broots[-1] - if options.latest: - opts['repoid'] = 'latest' - else: - opts['repoid'] = brootinfo['repo_id'] - opts['tag_name'] = brootinfo['tag_name'] - arch = brootinfo['arch'] - if not options.name: - options.name = "%s-task_%i" % (opts['tag_name'], task_id) - elif options.tag: - if not options.arch: - error("Please specify an arch") - tag = session.getTag(options.tag) - if not tag: - parser.error("No such tag: %s" % options.tag) - arch = options.arch - config = session.getBuildConfig(tag['id']) - if not config: - error("Could not get config info for tag: %(name)s" % tag) - opts['tag_name'] = tag['name'] - if options.latest: - opts['repoid'] = 'latest' - else: - repo = session.getRepo(config['id']) - if not repo: - error("Could not get a repo for tag: %(name)s" % tag) - opts['repoid'] = repo['id'] - elif options.target: - if not options.arch: - error("Please specify an arch") - arch = options.arch - target = session.getBuildTarget(options.target) - if not target: - parser.error("No such build target: %s" % options.target) - opts['tag_name'] = target['build_tag_name'] - if options.latest: - opts['repoid'] = 'latest' - else: - repo = session.getRepo(target['build_tag']) - if not repo: - error("Could not get a repo for tag: %s" % opts['tag_name']) - opts['repoid'] = repo['id'] - else: - parser.error("Please specify one of: --tag, --target, --task, --buildroot") - if options.name: - name = options.name - else: - name = "%(tag_name)s-repo_%(repoid)s" % opts - - event = None - if opts['repoid'] != 'latest': - event = session.repoInfo(opts['repoid'])['create_event'] - buildcfg = session.getBuildConfig(opts['tag_name'], event=event) - if arch: - if not buildcfg['arches']: - warn("Tag %s has an empty arch list" % opts['tag_name']) - elif arch not in buildcfg['arches']: - warn('%s is not in the list of tag arches' % arch) - if 'mock.forcearch' in buildcfg['extra']: - if bool(buildcfg['extra']['mock.forcearch']): - opts['forcearch'] = arch - if 'mock.package_manager' in buildcfg['extra']: - opts['package_manager'] = buildcfg['extra']['mock.package_manager'] - if 'mock.yum.module_hotfixes' in buildcfg['extra']: - opts['module_hotfixes'] = buildcfg['extra']['mock.yum.module_hotfixes'] - if 'mock.yum.best' in buildcfg['extra']: - opts['yum_best'] = int(buildcfg['extra']['mock.yum.best']) - if 'mock.bootstrap_image' in buildcfg['extra']: - opts['use_bootstrap_image'] = True - opts['bootstrap_image'] = buildcfg['extra']['mock.bootstrap_image'] - if 'mock.use_bootstrap' in buildcfg['extra']: - opts['use_bootstrap'] = buildcfg['extra']['mock.use_bootstrap'] - if 'mock.module_setup_commands' in buildcfg['extra']: - opts['module_setup_commands'] = buildcfg['extra']['mock.module_setup_commands'] - if 'mock.releasever' in buildcfg['extra']: - opts['releasever'] = buildcfg['extra']['mock.releasever'] - opts['tag_macros'] = {} - for key in buildcfg['extra']: - if key.startswith('rpm.macro.'): - macro = '%' + key[10:] - opts['tag_macros'][macro] = buildcfg['extra'][key] - output = koji.genMockConfig(name, arch, **opts) - if options.ofile: - with open(options.ofile, 'wt') as fo: - fo.write(output) - else: - print(output) - - diff --git a/cli/koji_cli/commands/cmd_moshimoshi.py b/cli/koji_cli/commands/cmd_moshimoshi.py deleted file mode 100644 index eabb8af..0000000 --- a/cli/koji_cli/commands/cmd_moshimoshi.py +++ /dev/null @@ -1,41 +0,0 @@ -from __future__ import absolute_import, division - -import random -from optparse import OptionParser - - -import koji - -from koji_cli.lib import ( - activate_session, - get_usage_str, - greetings -) - - -def handle_moshimoshi(options, session, args): - "[misc] Introduce yourself" - usage = "usage: %prog moshimoshi [options]" - parser = OptionParser(usage=get_usage_str(usage)) - (opts, args) = parser.parse_args(args) - if len(args) != 0: - parser.error("This command takes no arguments") - activate_session(session, options) - u = session.getLoggedInUser() - if not u: - print("Not authenticated") - u = {'name': 'anonymous user'} - print("%s, %s!" % (_printable_unicode(random.choice(greetings)), u["name"])) - print("") - print("You are using the hub at %s" % session.baseurl) - authtype = u.get('authtype', getattr(session, 'authtype', None)) - if authtype == koji.AUTHTYPE_NORMAL: - print("Authenticated via password") - elif authtype == koji.AUTHTYPE_GSSAPI: - print("Authenticated via GSSAPI") - elif authtype == koji.AUTHTYPE_KERB: - print("Authenticated via Kerberos principal %s" % session.krb_principal) - elif authtype == koji.AUTHTYPE_SSL: - print("Authenticated via client certificate %s" % options.cert) - - diff --git a/cli/koji_cli/commands/cmd_move_build.py b/cli/koji_cli/commands/cmd_move_build.py deleted file mode 100644 index 20c743a..0000000 --- a/cli/koji_cli/commands/cmd_move_build.py +++ /dev/null @@ -1,67 +0,0 @@ -from __future__ import absolute_import, division - -from optparse import OptionParser - - -import koji - -from koji_cli.lib import ( - _running_in_bg, - activate_session, - get_usage_str, - watch_tasks -) - - -def handle_move_build(opts, session, args): - "[bind] 'Move' one or more builds between tags" - usage = "usage: %prog move-build [options] [ ...]" - parser = OptionParser(usage=get_usage_str(usage)) - parser.add_option("--force", action="store_true", help="force operation") - parser.add_option("--wait", action="store_true", - help="Wait on tasks, even if running in the background") - parser.add_option("--nowait", action="store_false", dest="wait", - help="Do not wait on tasks") - parser.add_option("--all", action="store_true", - help="move all instances of a package, 's are package names") - (options, args) = parser.parse_args(args) - if len(args) < 3: - if options.all: - parser.error( - "This command, with --all, takes at least three arguments: two tags and one or " - "more package names") - else: - parser.error( - "This command takes at least three arguments: two tags and one or more package " - "n-v-r's") - activate_session(session, opts) - tasks = [] - builds = [] - - if options.all: - for arg in args[2:]: - pkg = session.getPackage(arg) - if not pkg: - print("No such package: %s, skipping." % arg) - continue - tasklist = session.moveAllBuilds(args[0], args[1], arg, options.force) - tasks.extend(tasklist) - else: - for arg in args[2:]: - build = session.getBuild(arg) - if not build: - print("No such build: %s, skipping." % arg) - continue - if build not in builds: - builds.append(build) - - for build in builds: - task_id = session.moveBuild(args[0], args[1], build['id'], options.force) - tasks.append(task_id) - print("Created task %d, moving %s" % (task_id, koji.buildLabel(build))) - if options.wait or (options.wait is None and not _running_in_bg()): - session.logout() - return watch_tasks(session, tasks, quiet=opts.quiet, - poll_interval=opts.poll_interval, topurl=opts.topurl) - - diff --git a/cli/koji_cli/commands/cmd_prune_signed_copies.py b/cli/koji_cli/commands/cmd_prune_signed_copies.py deleted file mode 100644 index fb4ac0c..0000000 --- a/cli/koji_cli/commands/cmd_prune_signed_copies.py +++ /dev/null @@ -1,373 +0,0 @@ -from __future__ import absolute_import, division - -import fnmatch -import os -import stat -import time -from optparse import SUPPRESS_HELP, OptionParser - -import six -import six.moves.xmlrpc_client -from six.moves import range, zip - -import koji -from koji.util import to_list - -from koji_cli.lib import ( - activate_session, - get_usage_str, - warn -) - - -def handle_prune_signed_copies(goptions, session, args): - "[admin] Prune signed copies" - usage = "usage: %prog prune-signed-copies [options]" - parser = OptionParser(usage=get_usage_str(usage)) - parser.add_option("-n", "--test", action="store_true", help="Test mode") - parser.add_option("-v", "--verbose", action="store_true", help="Be more verbose") - parser.add_option("--days", type="int", default=5, help="Timeout before clearing") - parser.add_option("-p", "--package", "--pkg", help="Limit to a single package") - parser.add_option("-b", "--build", help="Limit to a single build") - parser.add_option("-i", "--ignore-tag", action="append", default=[], - help="Ignore these tags when considering whether a build is/was latest") - parser.add_option("--ignore-tag-file", - help="File to read tag ignore patterns from") - parser.add_option("-r", "--protect-tag", action="append", default=[], - help="Do not prune signed copies from matching tags") - parser.add_option("--protect-tag-file", - help="File to read tag protect patterns from") - parser.add_option("--trashcan-tag", default="trashcan", help="Specify trashcan tag") - # Don't use local debug option, this one stays here for backward compatibility - # https://pagure.io/koji/issue/2084 - parser.add_option("--debug", action="store_true", default=goptions.debug, help=SUPPRESS_HELP) - (options, args) = parser.parse_args(args) - # different ideas/modes - # 1) remove all signed copies of builds that are not latest for some tag - # 2) remove signed copies when a 'better' signature is available - # 3) for a specified tag, remove all signed copies that are not latest (w/ inheritance) - # 4) for a specified tag, remove all signed copies (no inheritance) - # (but skip builds that are multiply tagged) - - # for now, we're just implementing mode #1 - # (with the modification that we check to see if the build was latest within - # the last N days) - if options.ignore_tag_file: - with open(options.ignore_tag_file) as fo: - options.ignore_tag.extend([line.strip() for line in fo.readlines()]) - if options.protect_tag_file: - with open(options.protect_tag_file) as fo: - options.protect_tag.extend([line.strip() for line in fo.readlines()]) - if options.debug: - options.verbose = True - cutoff_ts = time.time() - options.days * 24 * 3600 - if options.debug: - print("Cutoff date: %s" % time.asctime(time.localtime(cutoff_ts))) - activate_session(session, goptions) - if not options.build: - if options.verbose: - print("Getting builds...") - qopts = { - 'state': koji.BUILD_STATES['COMPLETE'], - 'queryOpts': { - 'limit': 50000, - 'offset': 0, - 'order': 'build_id', - } - } - if options.package: - pkginfo = session.getPackage(options.package) - qopts['packageID'] = pkginfo['id'] - builds = [] - while True: - chunk = [(b['nvr'], b) for b in session.listBuilds(**qopts)] - if not chunk: - break - builds.extend(chunk) - qopts['queryOpts']['offset'] += qopts['queryOpts']['limit'] - if options.verbose: - print("...got %i builds" % len(builds)) - builds.sort() - else: - # single build - binfo = session.getBuild(options.build) - if not binfo: - parser.error('No such build: %s' % options.build) - builds = [("%(name)s-%(version)s-%(release)s" % binfo, binfo)] - total_files = 0 - total_space = 0 - - def _histline(event_id, x): - if event_id == x['revoke_event']: - ts = x['revoke_ts'] - fmt = "Untagged %(name)s-%(version)s-%(release)s from %(tag_name)s" - elif event_id == x['create_event']: - ts = x['create_ts'] - fmt = "Tagged %(name)s-%(version)s-%(release)s with %(tag_name)s" - if x['active']: - fmt += " [still active]" - else: - raise koji.GenericError("No such event: (%r, %r)" % (event_id, x)) - time_str = time.asctime(time.localtime(ts)) - return "%s: %s" % (time_str, fmt % x) - for nvr, binfo in builds: - # listBuilds returns slightly different data than normal - if 'id' not in binfo: - binfo['id'] = binfo['build_id'] - if 'name' not in binfo: - binfo['name'] = binfo['package_name'] - if options.debug: - print("DEBUG: %s" % nvr) - # see how recently this build was latest for a tag - is_latest = False - is_protected = False - last_latest = None - tags = {} - for entry in session.queryHistory(build=binfo['id'])['tag_listing']: - # we used queryHistory rather than listTags so we can consider tags - # that the build was recently untagged from - tags.setdefault(entry['tag.name'], 1) - if options.debug: - print("Tags: %s" % to_list(tags.keys())) - for tag_name in tags: - if tag_name == options.trashcan_tag: - if options.debug: - print("Ignoring trashcan tag for build %s" % nvr) - continue - ignore_tag = False - for pattern in options.ignore_tag: - if fnmatch.fnmatch(tag_name, pattern): - if options.debug: - print("Ignoring tag %s for build %s" % (tag_name, nvr)) - ignore_tag = True - break - if ignore_tag: - continue - # in order to determine how recently this build was latest, we have - # to look at the tagging history. - hist = session.queryHistory(tag=tag_name, package=binfo['name'])['tag_listing'] - if not hist: - # really shouldn't happen - raise koji.GenericError("No history found for %s in %s" % (nvr, tag_name)) - timeline = [] - for x in hist: - # note that for revoked entries, we're effectively splitting them into - # two parts: creation and revocation. - timeline.append((x['create_event'], 1, x)) - # at the same event, revokes happen first - if x['revoke_event'] is not None: - timeline.append((x['revoke_event'], 0, x)) - timeline.sort(key=lambda entry: entry[:2]) - # find most recent creation entry for our build and crop there - latest_ts = None - for i in range(len(timeline) - 1, -1, -1): - # searching in reverse cronological order - event_id, is_create, entry = timeline[i] - if entry['build_id'] == binfo['id'] and is_create: - latest_ts = event_id - break - if not latest_ts: - # really shouldn't happen - raise koji.GenericError("No creation event found for %s in %s" % (nvr, tag_name)) - our_entry = entry - if options.debug: - print(_histline(event_id, our_entry)) - # now go through the events since most recent creation entry - timeline = timeline[i + 1:] - if not timeline: - is_latest = True - if options.debug: - print("%s is latest in tag %s" % (nvr, tag_name)) - break - # before we go any further, is this a protected tag? - protect_tag = False - for pattern in options.protect_tag: - if fnmatch.fnmatch(tag_name, pattern): - protect_tag = True - break - if protect_tag: - # we use the same time limit as for the latest calculation - # if this build was in this tag within that limit, then we will - # not prune its signed copies - if our_entry['revoke_event'] is None: - # we're still tagged with a protected tag - if options.debug: - print("Build %s has protected tag %s" % (nvr, tag_name)) - is_protected = True - break - elif our_entry['revoke_ts'] > cutoff_ts: - # we were still tagged here sometime before the cutoff - if options.debug: - print("Build %s had protected tag %s until %s" - % (nvr, tag_name, - time.asctime(time.localtime(our_entry['revoke_ts'])))) - is_protected = True - break - replaced_ts = None - revoke_ts = None - others = {} - for event_id, is_create, entry in timeline: - # So two things can knock this build from the title of latest: - # - it could be untagged (entry revoked) - # - another build could become latest (replaced) - # Note however that if the superceding entry is itself revoked, then - # our build could become latest again - if options.debug: - print(_histline(event_id, entry)) - if entry['build_id'] == binfo['id']: - if is_create: - # shouldn't happen - raise koji.GenericError("Duplicate creation event found for %s in %s" - % (nvr, tag_name)) - else: - # we've been revoked - revoke_ts = entry['revoke_ts'] - break - else: - if is_create: - # this build has become latest - replaced_ts = entry['create_ts'] - if entry['active']: - # this entry not revoked yet, so we're done for this tag - break - # since this entry is revoked later, our build might eventually be - # uncovered, so we have to keep looking - others[entry['build_id']] = 1 - else: - # other build revoked - # see if our build has resurfaced - if entry['build_id'] in others: - del others[entry['build_id']] - if replaced_ts is not None and not others: - # we've become latest again - # (note: we're not revoked yet because that triggers a break above) - replaced_ts = None - latest_ts = entry['revoke_ts'] - if last_latest is None: - timestamps = [] - else: - timestamps = [last_latest] - if revoke_ts is None: - if replaced_ts is None: - # turns out we are still latest - is_latest = True - if options.debug: - print("%s is latest (again) in tag %s" % (nvr, tag_name)) - break - else: - # replaced (but not revoked) - timestamps.append(replaced_ts) - if options.debug: - print("tag %s: %s not latest (replaced %s)" - % (tag_name, nvr, time.asctime(time.localtime(replaced_ts)))) - elif replaced_ts is None: - # revoked but not replaced - timestamps.append(revoke_ts) - if options.debug: - print("tag %s: %s not latest (revoked %s)" - % (tag_name, nvr, time.asctime(time.localtime(revoke_ts)))) - else: - # revoked AND replaced - timestamps.append(min(revoke_ts, replaced_ts)) - if options.debug: - print("tag %s: %s not latest (revoked %s, replaced %s)" - % (tag_name, nvr, time.asctime(time.localtime(revoke_ts)), - time.asctime(time.localtime(replaced_ts)))) - last_latest = max(timestamps) - if last_latest > cutoff_ts: - if options.debug: - print("%s was latest past the cutoff" % nvr) - is_latest = True - break - if is_latest: - continue - if is_protected: - continue - # not latest anywhere since cutoff, so we can remove all signed copies - rpms = session.listRPMs(buildID=binfo['id']) - session.multicall = True - for rpminfo in rpms: - session.queryRPMSigs(rpm_id=rpminfo['id']) - by_sig = {} - # index by sig - for rpminfo, [sigs] in zip(rpms, session.multiCall()): - for sig in sigs: - sigkey = sig['sigkey'] - by_sig.setdefault(sigkey, []).append(rpminfo) - builddir = koji.pathinfo.build(binfo) - build_files = 0 - build_space = 0 - if not by_sig and options.debug: - print("(build has no signatures)") - for sigkey, rpms in six.iteritems(by_sig): - mycount = 0 - archdirs = {} - sigdirs = {} - for rpminfo in rpms: - signedpath = "%s/%s" % (builddir, koji.pathinfo.signed(rpminfo, sigkey)) - try: - st = os.lstat(signedpath) - except OSError: - continue - if not stat.S_ISREG(st.st_mode): - # warn about this - print("Skipping %s. Not a regular file" % signedpath) - continue - if st.st_mtime > cutoff_ts: - print("Skipping %s. File newer than cutoff" % signedpath) - continue - if options.test: - print("Would have unlinked: %s" % signedpath) - else: - if options.verbose: - print("Unlinking: %s" % signedpath) - try: - os.unlink(signedpath) - except OSError as e: - print("Error removing %s: %s" % (signedpath, e)) - print("This script needs write access to %s" % koji.BASEDIR) - continue - mycount += 1 - build_files += 1 - build_space += st.st_size - # XXX - this makes some layout assumptions, but - # pathinfo doesn't report what we need - mydir = os.path.dirname(signedpath) - archdirs[mydir] = 1 - sigdirs[os.path.dirname(mydir)] = 1 - for dir in archdirs: - if options.test: - print("Would have removed dir: %s" % dir) - else: - if options.verbose: - print("Removing dir: %s" % dir) - try: - os.rmdir(dir) - except OSError as e: - print("Error removing %s: %s" % (signedpath, e)) - if len(sigdirs) == 1: - dir = to_list(sigdirs.keys())[0] - if options.test: - print("Would have removed dir: %s" % dir) - else: - if options.verbose: - print("Removing dir: %s" % dir) - try: - os.rmdir(dir) - except OSError as e: - print("Error removing %s: %s" % (signedpath, e)) - elif len(sigdirs) > 1: - warn("More than one signature dir for %s: %r" % (sigkey, sigdirs)) - if build_files: - total_files += build_files - total_space += build_space - if options.verbose: - print("Build: %s, Removed %i signed copies (%i bytes). Total: %i/%i" - % (nvr, build_files, build_space, total_files, total_space)) - elif options.debug and by_sig: - print("(build has no signed copies)") - print("--- Grand Totals ---") - print("Files: %i" % total_files) - print("Bytes: %i" % total_space) - - diff --git a/cli/koji_cli/commands/cmd_regen_repo.py b/cli/koji_cli/commands/cmd_regen_repo.py deleted file mode 100644 index 954f043..0000000 --- a/cli/koji_cli/commands/cmd_regen_repo.py +++ /dev/null @@ -1,73 +0,0 @@ -from __future__ import absolute_import, division - -from optparse import OptionParser - - - -from koji_cli.lib import ( - _running_in_bg, - activate_session, - get_usage_str, - warn, - watch_tasks -) - - -def handle_regen_repo(options, session, args): - "[admin] Force a repo to be regenerated" - usage = "usage: %prog regen-repo [options] " - parser = OptionParser(usage=get_usage_str(usage)) - parser.add_option("--target", action="store_true", - help="Interpret the argument as a build target name") - parser.add_option("--wait", action="store_true", - help="Wait on for regen to finish, even if running in the background") - parser.add_option("--nowait", action="store_false", dest="wait", - help="Don't wait on for regen to finish") - parser.add_option("--debuginfo", action="store_true", help="Include debuginfo rpms in repo") - parser.add_option("--source", "--src", action="store_true", - help="Include source rpms in each of repos") - parser.add_option("--separate-source", "--separate-src", action="store_true", - help="Include source rpms in separate src repo") - (suboptions, args) = parser.parse_args(args) - if len(args) == 0: - parser.error("A tag name must be specified") - elif len(args) > 1: - if suboptions.target: - parser.error("Only a single target may be specified") - else: - parser.error("Only a single tag name may be specified") - activate_session(session, options) - tag = args[0] - repo_opts = {} - if suboptions.target: - info = session.getBuildTarget(tag) - if not info: - parser.error("No such build target: %s" % tag) - tag = info['build_tag_name'] - info = session.getTag(tag, strict=True) - else: - info = session.getTag(tag) - if not info: - parser.error("No such tag: %s" % tag) - tag = info['name'] - targets = session.getBuildTargets(buildTagID=info['id']) - if not targets: - warn("%s is not a build tag" % tag) - if not info['arches']: - warn("Tag %s has an empty arch list" % info['name']) - if suboptions.debuginfo: - repo_opts['debuginfo'] = True - if suboptions.source: - repo_opts['src'] = True - if suboptions.separate_source: - repo_opts['separate_src'] = True - task_id = session.newRepo(tag, **repo_opts) - print("Regenerating repo for tag: %s" % tag) - print("Created task: %d" % task_id) - print("Task info: %s/taskinfo?taskID=%s" % (options.weburl, task_id)) - if suboptions.wait or (suboptions.wait is None and not _running_in_bg()): - session.logout() - return watch_tasks(session, [task_id], quiet=options.quiet, - poll_interval=options.poll_interval, topurl=options.topurl) - - diff --git a/cli/koji_cli/commands/cmd_remove_channel.py b/cli/koji_cli/commands/cmd_remove_channel.py deleted file mode 100644 index 50ad595..0000000 --- a/cli/koji_cli/commands/cmd_remove_channel.py +++ /dev/null @@ -1,29 +0,0 @@ -from __future__ import absolute_import, division - -from optparse import OptionParser - - - -from koji_cli.lib import ( - activate_session, - error, - get_usage_str -) - - -def handle_remove_channel(goptions, session, args): - "[admin] Remove a channel entirely" - usage = "usage: %prog remove-channel [options] " - parser = OptionParser(usage=get_usage_str(usage)) - parser.add_option("--force", action="store_true", help="force removal, if possible") - (options, args) = parser.parse_args(args) - print("remove-channel is deprecated and will be removed in 1.30") - if len(args) != 1: - parser.error("Incorrect number of arguments") - activate_session(session, goptions) - cinfo = session.getChannel(args[0]) - if not cinfo: - error("No such channel: %s" % args[0]) - session.removeChannel(args[0], force=options.force) - - diff --git a/cli/koji_cli/commands/cmd_remove_external_repo.py b/cli/koji_cli/commands/cmd_remove_external_repo.py deleted file mode 100644 index b1f41c2..0000000 --- a/cli/koji_cli/commands/cmd_remove_external_repo.py +++ /dev/null @@ -1,56 +0,0 @@ -from __future__ import absolute_import, division - -from optparse import OptionParser - - - -from koji_cli.lib import ( - activate_session, - error, - get_usage_str, - warn -) - - -def handle_remove_external_repo(goptions, session, args): - "[admin] Remove an external repo from a tag or tags, or remove entirely" - usage = "usage: %prog remove-external-repo [ ...]" - parser = OptionParser(usage=get_usage_str(usage)) - parser.add_option("--alltags", action="store_true", help="Remove from all tags") - parser.add_option("--force", action='store_true', help="Force action") - (options, args) = parser.parse_args(args) - if len(args) < 1: - parser.error("Incorrect number of arguments") - activate_session(session, goptions) - repo = args[0] - tags = args[1:] - delete = not bool(tags) - data = session.getTagExternalRepos(repo_info=repo) - current_tags = [d['tag_name'] for d in data] - if options.alltags: - delete = False - if tags: - parser.error("Do not specify tags when using --alltags") - if not current_tags: - if options.force: - delete = True - else: - warn("External repo %s not associated with any tags" % repo) - return 0 - tags = current_tags - if delete: - # removing entirely - if current_tags and not options.force: - warn("Error: external repo %s used by tag(s): %s" % (repo, ', '.join(current_tags))) - error("Use --force to remove anyway") - session.deleteExternalRepo(args[0]) - else: - for tag in tags: - if tag not in current_tags: - print("External repo %s not associated with tag %s" % (repo, tag)) - continue - session.removeExternalRepoFromTag(tag, repo) - - -# This handler is for spinning livecd images -# diff --git a/cli/koji_cli/commands/cmd_remove_group.py b/cli/koji_cli/commands/cmd_remove_group.py deleted file mode 100644 index 77b1fc2..0000000 --- a/cli/koji_cli/commands/cmd_remove_group.py +++ /dev/null @@ -1,39 +0,0 @@ -from __future__ import absolute_import, division - -from optparse import OptionParser - - - -from koji_cli.lib import ( - activate_session, - error, - get_usage_str -) - - -def handle_remove_group(goptions, session, args): - "[admin] Remove group from tag" - usage = "usage: %prog remove-group " - parser = OptionParser(usage=get_usage_str(usage)) - (options, args) = parser.parse_args(args) - if len(args) != 2: - parser.error("Please specify a tag name and a group name") - tag = args[0] - group = args[1] - - activate_session(session, goptions) - if not (session.hasPerm('admin') or session.hasPerm('tag')): - parser.error("This action requires tag or admin privileges") - - dsttag = session.getTag(tag) - if not dsttag: - error("No such tag: %s" % tag) - - groups = dict([(p['name'], p['group_id']) for p in session.getTagGroups(tag, inherit=False)]) - group_id = groups.get(group, None) - if group_id is None: - error("Group %s doesn't exist within tag %s" % (group, tag)) - - session.groupListRemove(tag, group) - - diff --git a/cli/koji_cli/commands/cmd_remove_host_from_channel.py b/cli/koji_cli/commands/cmd_remove_host_from_channel.py deleted file mode 100644 index 6241fb8..0000000 --- a/cli/koji_cli/commands/cmd_remove_host_from_channel.py +++ /dev/null @@ -1,34 +0,0 @@ -from __future__ import absolute_import, division - -from optparse import OptionParser - - - -from koji_cli.lib import ( - activate_session, - error, - get_usage_str -) - - -def handle_remove_host_from_channel(goptions, session, args): - "[admin] Remove a host from a channel" - usage = "usage: %prog remove-host-from-channel [options] " - parser = OptionParser(usage=get_usage_str(usage)) - (options, args) = parser.parse_args(args) - if len(args) != 2: - parser.error("Please specify a hostname and a channel") - host = args[0] - activate_session(session, goptions) - hostinfo = session.getHost(host) - if not hostinfo: - error("No such host: %s" % host) - hostchannels = [c['name'] for c in session.listChannels(hostinfo['id'])] - - channel = args[1] - if channel not in hostchannels: - error("Host %s is not a member of channel %s" % (host, channel)) - - session.removeHostFromChannel(host, channel) - - diff --git a/cli/koji_cli/commands/cmd_remove_notification.py b/cli/koji_cli/commands/cmd_remove_notification.py deleted file mode 100644 index 7e07b6c..0000000 --- a/cli/koji_cli/commands/cmd_remove_notification.py +++ /dev/null @@ -1,34 +0,0 @@ -from __future__ import absolute_import, division - -from optparse import OptionParser - - - -from koji_cli.lib import ( - activate_session, - get_usage_str -) - - -def handle_remove_notification(goptions, session, args): - "[monitor] Remove user's notifications" - usage = "usage: %prog remove-notification [options] [ ...]" - parser = OptionParser(usage=get_usage_str(usage)) - (options, args) = parser.parse_args(args) - - activate_session(session, goptions) - - if len(args) < 1: - parser.error("At least one notification id has to be specified") - - try: - n_ids = [int(x) for x in args] - except ValueError: - parser.error("All notification ids has to be integers") - - for n_id in n_ids: - session.deleteNotification(n_id) - if not goptions.quiet: - print("Notification %d successfully removed." % n_id) - - diff --git a/cli/koji_cli/commands/cmd_remove_pkg.py b/cli/koji_cli/commands/cmd_remove_pkg.py deleted file mode 100644 index 1956214..0000000 --- a/cli/koji_cli/commands/cmd_remove_pkg.py +++ /dev/null @@ -1,51 +0,0 @@ -from __future__ import absolute_import, division - -from optparse import OptionParser - - -import koji - -from koji_cli.lib import ( - activate_session, - error, - get_usage_str, - warn -) - - -def handle_remove_pkg(goptions, session, args): - "[admin] Remove a package from the listing for tag" - usage = "usage: %prog remove-pkg [options] [ ...]" - parser = OptionParser(usage=get_usage_str(usage)) - parser.add_option("--force", action='store_true', help="Override blocks if necessary") - (options, args) = parser.parse_args(args) - if len(args) < 2: - parser.error("Please specify a tag and at least one package") - activate_session(session, goptions) - tag = args[0] - opts = {} - opts['force'] = options.force - # check if list of packages exists for that tag already - dsttag = session.getTag(tag) - if dsttag is None: - error("No such tag: %s" % tag) - try: - pkglist = session.listPackages(tagID=dsttag['id'], with_owners=False) - except koji.ParameterError: - # performance option added in 1.25 - pkglist = session.listPackages(tagID=dsttag['id']) - pkglist = dict([(p['package_name'], p['package_id']) for p in pkglist]) - ret = 0 - for package in args[1:]: - package_id = pkglist.get(package, None) - if package_id is None: - warn("Package %s is not in tag %s" % (package, tag)) - ret = 1 - if ret: - error(code=ret) - session.multicall = True - for package in args[1:]: - session.packageListRemove(tag, package, **opts) - session.multiCall(strict=True) - - diff --git a/cli/koji_cli/commands/cmd_remove_sig.py b/cli/koji_cli/commands/cmd_remove_sig.py deleted file mode 100644 index 4d19dfa..0000000 --- a/cli/koji_cli/commands/cmd_remove_sig.py +++ /dev/null @@ -1,47 +0,0 @@ -from __future__ import absolute_import, division - -from optparse import OptionParser - - -import koji - -from koji_cli.lib import ( - activate_session, - error, - get_usage_str -) - - -def handle_remove_sig(goptions, session, args): - "[admin] Remove signed RPMs from db and disk" - usage = "usage: %prog remove-sig [options] " - description = "Only use this method in extreme situations, because it " - description += "goes against Koji's design of immutable, auditable data." - parser = OptionParser(usage=get_usage_str(usage), description=description) - parser.add_option("--sigkey", action="store", default=None, help="Specify signature key") - parser.add_option("--all", action="store_true", default=False, - help="Remove all signed copies for specified RPM") - (options, args) = parser.parse_args(args) - if len(args) < 1: - parser.error("Please specify an RPM") - - if not options.all and not options.sigkey: - error("Either --sigkey or --all options must be given") - - if options.all and options.sigkey: - error("Conflicting options specified") - - activate_session(session, goptions) - rpminfo = args[0] - - try: - session.deleteRPMSig(rpminfo, sigkey=options.sigkey, all_sigs=options.all) - except koji.GenericError as e: - msg = str(e) - if msg.startswith("No such rpm"): - # make this a little more readable than the hub error - error("No such rpm in system: %s" % rpminfo) - else: - error("Signature removal failed: %s" % msg) - - diff --git a/cli/koji_cli/commands/cmd_remove_tag.py b/cli/koji_cli/commands/cmd_remove_tag.py deleted file mode 100644 index 12ce40a..0000000 --- a/cli/koji_cli/commands/cmd_remove_tag.py +++ /dev/null @@ -1,34 +0,0 @@ -from __future__ import absolute_import, division - -from optparse import OptionParser - - - -from koji_cli.lib import ( - activate_session, - error, - get_usage_str -) - - -def handle_remove_tag(goptions, session, args): - "[admin] Remove a tag" - usage = "usage: %prog remove-tag [options] " - parser = OptionParser(usage=get_usage_str(usage)) - (options, args) = parser.parse_args(args) - - if len(args) != 1: - parser.error("Please specify a tag to remove") - activate_session(session, goptions) - - if not (session.hasPerm('admin') or session.hasPerm('tag')): - parser.error("This action requires tag or admin privileges") - - tag = args[0] - tag_info = session.getTag(tag) - if not tag_info: - error("No such tag: %s" % tag) - - session.deleteTag(tag_info['id']) - - diff --git a/cli/koji_cli/commands/cmd_remove_tag_inheritance.py b/cli/koji_cli/commands/cmd_remove_tag_inheritance.py deleted file mode 100644 index 718da62..0000000 --- a/cli/koji_cli/commands/cmd_remove_tag_inheritance.py +++ /dev/null @@ -1,71 +0,0 @@ -from __future__ import absolute_import, division - -from optparse import OptionParser - - - -from koji_cli.lib import ( - activate_session, - error, - get_usage_str -) - - -def handle_remove_tag_inheritance(goptions, session, args): - """[admin] Remove a tag inheritance link""" - usage = "usage: %prog remove-tag-inheritance " - parser = OptionParser(usage=get_usage_str(usage)) - (options, args) = parser.parse_args(args) - - if len(args) < 1: - parser.error("This command takes at least one argument: a tag name or ID") - - if len(args) > 3: - parser.error("This command takes at most three argument: a tag name or ID, a parent tag " - "name or ID, and a priority") - - activate_session(session, goptions) - - tag = session.getTag(args[0]) - if not tag: - parser.error("No such tag: %s" % args[0]) - - parent = None - priority = None - if len(args) > 1: - parent = session.getTag(args[1]) - if not parent: - parser.error("No such tag: %s" % args[1]) - if len(args) > 2: - priority = args[2] - - data = session.getInheritanceData(tag['id']) - if parent and data: - data = [datum for datum in data if datum['parent_id'] == parent['id']] - if priority and data: - data = [datum for datum in data if datum['priority'] == priority] - - if len(data) == 0: - error("No inheritance link found to remove. Please check your arguments") - elif len(data) > 1: - print("Multiple matches for tag.") - if not parent: - error("Please specify a parent on the command line.") - if not priority: - error("Please specify a priority on the command line.") - error("Error: Key constraints may be broken. Exiting.") - - # len(data) == 1 - data = data[0] - - inheritanceData = session.getInheritanceData(tag['id']) - - new_data = data.copy() - new_data['delete link'] = True - - # find the data we want to edit and replace it - index = inheritanceData.index(data) - inheritanceData[index] = new_data - session.setInheritanceData(tag['id'], inheritanceData) - - diff --git a/cli/koji_cli/commands/cmd_remove_target.py b/cli/koji_cli/commands/cmd_remove_target.py deleted file mode 100644 index ab7e437..0000000 --- a/cli/koji_cli/commands/cmd_remove_target.py +++ /dev/null @@ -1,34 +0,0 @@ -from __future__ import absolute_import, division - -from optparse import OptionParser - - - -from koji_cli.lib import ( - activate_session, - error, - get_usage_str -) - - -def handle_remove_target(goptions, session, args): - "[admin] Remove a build target" - usage = "usage: %prog remove-target [options] " - parser = OptionParser(usage=get_usage_str(usage)) - (options, args) = parser.parse_args(args) - - if len(args) != 1: - parser.error("Please specify a build target to remove") - activate_session(session, goptions) - - if not (session.hasPerm('admin') or session.hasPerm('target')): - parser.error("This action requires target or admin privileges") - - target = args[0] - target_info = session.getBuildTarget(target) - if not target_info: - error("No such build target: %s" % target) - - session.deleteBuildTarget(target_info['id']) - - diff --git a/cli/koji_cli/commands/cmd_restart_hosts.py b/cli/koji_cli/commands/cmd_restart_hosts.py deleted file mode 100644 index 5c65d85..0000000 --- a/cli/koji_cli/commands/cmd_restart_hosts.py +++ /dev/null @@ -1,69 +0,0 @@ -from __future__ import absolute_import, division - -from optparse import OptionParser - - -import koji - -from koji_cli.lib import ( - _running_in_bg, - activate_session, - error, - get_usage_str, - warn, - watch_tasks -) - - -def handle_restart_hosts(options, session, args): - "[admin] Restart enabled hosts" - usage = "usage: %prog restart-hosts [options]" - parser = OptionParser(usage=get_usage_str(usage)) - parser.add_option("--wait", action="store_true", - help="Wait on the task, even if running in the background") - parser.add_option("--nowait", action="store_false", dest="wait", help="Don't wait on task") - parser.add_option("--quiet", action="store_true", - help="Do not print the task information", default=options.quiet) - parser.add_option("--force", action="store_true", help="Ignore checks and force operation") - parser.add_option("--channel", help="Only hosts in this channel") - parser.add_option("--arch", "-a", action="append", default=[], - help="Limit to hosts of this architecture (can be given multiple times)") - parser.add_option("--timeout", metavar='N', type='int', help="Time out after N seconds") - (my_opts, args) = parser.parse_args(args) - - if len(args) > 0: - parser.error("restart-hosts does not accept arguments") - - activate_session(session, options) - - # check for existing restart tasks - if not my_opts.force: - query = { - 'method': 'restartHosts', - 'state': - [koji.TASK_STATES[s] for s in ('FREE', 'OPEN', 'ASSIGNED')], - } - others = session.listTasks(query) - if others: - warn('Found other restartHosts tasks running.') - warn('Task ids: %r' % [t['id'] for t in others]) - error('Use --force to run anyway') - - callopts = {} - if my_opts.channel: - callopts['channel'] = my_opts.channel - if my_opts.arch: - callopts['arches'] = my_opts.arch - if my_opts.timeout: - callopts['timeout'] = my_opts.timeout - if callopts: - task_id = session.restartHosts(options=callopts) - else: - # allow default case to work with older hub - task_id = session.restartHosts() - if my_opts.wait or (my_opts.wait is None and not _running_in_bg()): - session.logout() - return watch_tasks(session, [task_id], quiet=my_opts.quiet, - poll_interval=options.poll_interval, topurl=options.topurl) - - diff --git a/cli/koji_cli/commands/cmd_resubmit.py b/cli/koji_cli/commands/cmd_resubmit.py deleted file mode 100644 index a1d52c2..0000000 --- a/cli/koji_cli/commands/cmd_resubmit.py +++ /dev/null @@ -1,42 +0,0 @@ -from __future__ import absolute_import, division - -from optparse import OptionParser - - - -from koji_cli.lib import ( - _running_in_bg, - activate_session, - get_usage_str, - watch_tasks -) - - -def handle_resubmit(goptions, session, args): - """[build] Retry a canceled or failed task, using the same parameter as the original task.""" - usage = "usage: %prog resubmit [options] " - parser = OptionParser(usage=get_usage_str(usage)) - parser.add_option("--wait", action="store_true", - help="Wait on task, even if running in the background") - parser.add_option("--nowait", action="store_false", dest="wait", help="Don't wait on task") - parser.add_option("--nowatch", action="store_true", dest="nowait", - help="An alias for --nowait") - parser.add_option("--quiet", action="store_true", default=goptions.quiet, - help="Do not print the task information") - (options, args) = parser.parse_args(args) - if len(args) != 1: - parser.error("Please specify a single task ID") - activate_session(session, goptions) - taskID = int(args[0]) - if not options.quiet: - print("Resubmitting the following task:") - _printTaskInfo(session, taskID, goptions.topdir, 0, False, True) - newID = session.resubmitTask(taskID) - if not options.quiet: - print("Resubmitted task %s as new task %s" % (taskID, newID)) - if options.wait or (options.wait is None and not _running_in_bg()): - session.logout() - return watch_tasks(session, [newID], quiet=options.quiet, - poll_interval=goptions.poll_interval, topurl=goptions.topurl) - - diff --git a/cli/koji_cli/commands/cmd_revoke_cg_access.py b/cli/koji_cli/commands/cmd_revoke_cg_access.py deleted file mode 100644 index 2c6ca4b..0000000 --- a/cli/koji_cli/commands/cmd_revoke_cg_access.py +++ /dev/null @@ -1,28 +0,0 @@ -from __future__ import absolute_import, division - -from optparse import OptionParser - - - -from koji_cli.lib import ( - activate_session, - get_usage_str -) - - -def handle_revoke_cg_access(goptions, session, args): - "[admin] Remove a user from a content generator" - usage = "usage: %prog revoke-cg-access " - parser = OptionParser(usage=get_usage_str(usage)) - (options, args) = parser.parse_args(args) - if len(args) != 2: - parser.error("Please specify a user and content generator") - activate_session(session, goptions) - user = args[0] - cg = args[1] - uinfo = session.getUser(user) - if uinfo is None: - parser.error("No such user: %s" % user) - session.revokeCGAccess(uinfo['name'], cg) - - diff --git a/cli/koji_cli/commands/cmd_revoke_permission.py b/cli/koji_cli/commands/cmd_revoke_permission.py deleted file mode 100644 index 9917a30..0000000 --- a/cli/koji_cli/commands/cmd_revoke_permission.py +++ /dev/null @@ -1,32 +0,0 @@ -from __future__ import absolute_import, division - -from optparse import OptionParser - - - -from koji_cli.lib import ( - activate_session, - get_usage_str -) - - -def handle_revoke_permission(goptions, session, args): - "[admin] Revoke a permission from a user" - usage = "usage: %prog revoke-permission [ ...]" - parser = OptionParser(usage=get_usage_str(usage)) - (options, args) = parser.parse_args(args) - if len(args) < 2: - parser.error("Please specify a permission and at least one user") - activate_session(session, goptions) - perm = args[0] - names = args[1:] - users = [] - for n in names: - user = session.getUser(n) - if user is None: - parser.error("No such user: %s" % n) - users.append(user) - for user in users: - session.revokePermission(user['name'], perm) - - diff --git a/cli/koji_cli/commands/cmd_rpminfo.py b/cli/koji_cli/commands/cmd_rpminfo.py deleted file mode 100644 index 74263fc..0000000 --- a/cli/koji_cli/commands/cmd_rpminfo.py +++ /dev/null @@ -1,94 +0,0 @@ -from __future__ import absolute_import, division - -import os -import time -from optparse import OptionParser - - -import koji - -from koji_cli.lib import ( - ensure_connection, - error, - get_usage_str, - warn -) - - -def anon_handle_rpminfo(goptions, session, args): - "[info] Print basic information about an RPM" - usage = "usage: %prog rpminfo [options] [ ...]" - parser = OptionParser(usage=get_usage_str(usage)) - parser.add_option("--buildroots", action="store_true", - help="show buildroots the rpm was used in") - (options, args) = parser.parse_args(args) - if len(args) < 1: - parser.error("Please specify an RPM") - ensure_connection(session, goptions) - error_hit = False - for rpm in args: - info = session.getRPM(rpm) - if info is None: - warn("No such rpm: %s\n" % rpm) - error_hit = True - continue - if info['epoch'] is None: - info['epoch'] = "" - else: - info['epoch'] = str(info['epoch']) + ":" - if not info.get('external_repo_id', 0): - buildinfo = session.getBuild(info['build_id']) - buildinfo['name'] = buildinfo['package_name'] - buildinfo['arch'] = 'src' - if buildinfo['epoch'] is None: - buildinfo['epoch'] = "" - else: - buildinfo['epoch'] = str(buildinfo['epoch']) + ":" - print("RPM: %(epoch)s%(name)s-%(version)s-%(release)s.%(arch)s [%(id)d]" % info) - if info.get('external_repo_id'): - repo = session.getExternalRepo(info['external_repo_id']) - print("External Repository: %(name)s [%(id)i]" % repo) - print("External Repository url: %(url)s" % repo) - else: - print("RPM Path: %s" % - os.path.join(koji.pathinfo.build(buildinfo), koji.pathinfo.rpm(info))) - print("SRPM: %(epoch)s%(name)s-%(version)s-%(release)s [%(id)d]" % buildinfo) - print("SRPM Path: %s" % - os.path.join(koji.pathinfo.build(buildinfo), koji.pathinfo.rpm(buildinfo))) - print("Built: %s" % time.strftime('%a, %d %b %Y %H:%M:%S %Z', - time.localtime(info['buildtime']))) - print("SIGMD5: %(payloadhash)s" % info) - print("Size: %(size)s" % info) - if not info.get('external_repo_id', 0): - headers = session.getRPMHeaders(rpmID=info['id'], - headers=["license"]) - if 'license' in headers: - print("License: %(license)s" % headers) - print("Build ID: %(build_id)s" % info) - if info['buildroot_id'] is None: - print("No buildroot data available") - else: - br_info = session.getBuildroot(info['buildroot_id']) - if br_info['br_type'] == koji.BR_TYPES['STANDARD']: - print("Buildroot: %(id)i (tag %(tag_name)s, arch %(arch)s, repo %(repo_id)i)" % - br_info) - print("Build Host: %(host_name)s" % br_info) - print("Build Task: %(task_id)i" % br_info) - else: - print("Content generator: %(cg_name)s" % br_info) - print("Buildroot: %(id)i" % br_info) - print("Build Host OS: %(host_os)s (%(host_arch)s)" % br_info) - if info.get('extra'): - print("Extra: %(extra)r" % info) - if options.buildroots: - br_list = session.listBuildroots(rpmID=info['id'], queryOpts={'order': 'buildroot.id'}) - print("Used in %i buildroots:" % len(br_list)) - if len(br_list): - print(" %8s %-28s %-8s %-29s" % ('id', 'build tag', 'arch', 'build host')) - print(" %s %s %s %s" % ('-' * 8, '-' * 28, '-' * 8, '-' * 29)) - for br_info in br_list: - print(" %(id)8i %(tag_name)-28s %(arch)-8s %(host_name)-29s" % br_info) - if error_hit: - error() - - diff --git a/cli/koji_cli/commands/cmd_search.py b/cli/koji_cli/commands/cmd_search.py deleted file mode 100644 index e145060..0000000 --- a/cli/koji_cli/commands/cmd_search.py +++ /dev/null @@ -1,39 +0,0 @@ -from __future__ import absolute_import, division - -from optparse import OptionParser - - - -from koji_cli.lib import ( - ensure_connection, - get_usage_str -) - - -def anon_handle_search(goptions, session, args): - "[search] Search the system" - usage = "usage: %prog search [options] " - usage += '\nAvailable search types: %s' % ', '.join(_search_types) - parser = OptionParser(usage=get_usage_str(usage)) - parser.add_option("-r", "--regex", action="store_true", help="treat pattern as regex") - parser.add_option("--exact", action="store_true", help="exact matches only") - (options, args) = parser.parse_args(args) - if len(args) < 1: - parser.error("Please specify search type") - if len(args) < 2: - parser.error("Please specify search pattern") - type = args[0] - if type not in _search_types: - parser.error("No such search type: %s" % type) - pattern = args[1] - matchType = 'glob' - if options.regex: - matchType = 'regexp' - elif options.exact: - matchType = 'exact' - ensure_connection(session, goptions) - data = session.search(pattern, type, matchType) - for row in data: - print(row['name']) - - diff --git a/cli/koji_cli/commands/cmd_set_build_volume.py b/cli/koji_cli/commands/cmd_set_build_volume.py deleted file mode 100644 index 2ac5621..0000000 --- a/cli/koji_cli/commands/cmd_set_build_volume.py +++ /dev/null @@ -1,42 +0,0 @@ -from __future__ import absolute_import, division - -from optparse import OptionParser - - - -from koji_cli.lib import ( - activate_session, - error, - get_usage_str -) - - -def handle_set_build_volume(goptions, session, args): - "[admin] Move a build to a different volume" - usage = "usage: %prog set-build-volume [ ...]" - parser = OptionParser(usage=get_usage_str(usage)) - parser.add_option("-v", "--verbose", action="store_true", help="Be verbose") - (options, args) = parser.parse_args(args) - if len(args) < 2: - parser.error("You must provide a volume and at least one build") - volinfo = session.getVolume(args[0]) - if not volinfo: - error("No such volume: %s" % args[0]) - activate_session(session, goptions) - builds = [] - for nvr in args[1:]: - binfo = session.getBuild(nvr) - if not binfo: - print("No such build: %s" % nvr) - elif binfo['volume_id'] == volinfo['id']: - print("Build %s already on volume %s" % (nvr, volinfo['name'])) - else: - builds.append(binfo) - if not builds: - error("No builds to move") - for binfo in builds: - session.changeBuildVolume(binfo['id'], volinfo['id']) - if options.verbose: - print("%s: %s -> %s" % (binfo['nvr'], binfo['volume_name'], volinfo['name'])) - - diff --git a/cli/koji_cli/commands/cmd_set_pkg_arches.py b/cli/koji_cli/commands/cmd_set_pkg_arches.py deleted file mode 100644 index c149800..0000000 --- a/cli/koji_cli/commands/cmd_set_pkg_arches.py +++ /dev/null @@ -1,29 +0,0 @@ -from __future__ import absolute_import, division - -from optparse import OptionParser - - -import koji - -from koji_cli.lib import ( - activate_session, - get_usage_str -) - - -def handle_set_pkg_arches(goptions, session, args): - "[admin] Set the list of extra arches for a package" - usage = "usage: %prog set-pkg-arches [options] [ ...]" - parser = OptionParser(usage=get_usage_str(usage)) - parser.add_option("--force", action='store_true', help="Force operation") - (options, args) = parser.parse_args(args) - if len(args) < 3: - parser.error("Please specify an archlist, a tag, and at least one package") - activate_session(session, goptions) - arches = koji.parse_arches(args[0]) - tag = args[1] - with session.multicall(strict=True) as m: - for package in args[2:]: - m.packageListSetArches(tag, package, arches, force=options.force) - - diff --git a/cli/koji_cli/commands/cmd_set_pkg_owner.py b/cli/koji_cli/commands/cmd_set_pkg_owner.py deleted file mode 100644 index d20890b..0000000 --- a/cli/koji_cli/commands/cmd_set_pkg_owner.py +++ /dev/null @@ -1,28 +0,0 @@ -from __future__ import absolute_import, division - -from optparse import OptionParser - - - -from koji_cli.lib import ( - activate_session, - get_usage_str -) - - -def handle_set_pkg_owner(goptions, session, args): - "[admin] Set the owner for a package" - usage = "usage: %prog set-pkg-owner [options] [ ...]" - parser = OptionParser(usage=get_usage_str(usage)) - parser.add_option("--force", action='store_true', help="Force operation") - (options, args) = parser.parse_args(args) - if len(args) < 3: - parser.error("Please specify an owner, a tag, and at least one package") - activate_session(session, goptions) - owner = args[0] - tag = args[1] - with session.multicall(strict=True) as m: - for package in args[2:]: - m.packageListSetOwner(tag, package, owner, force=options.force) - - diff --git a/cli/koji_cli/commands/cmd_set_pkg_owner_global.py b/cli/koji_cli/commands/cmd_set_pkg_owner_global.py deleted file mode 100644 index c5ec5d7..0000000 --- a/cli/koji_cli/commands/cmd_set_pkg_owner_global.py +++ /dev/null @@ -1,70 +0,0 @@ -from __future__ import absolute_import, division - -from optparse import OptionParser - - - -from koji_cli.lib import ( - activate_session, - error, - get_usage_str -) - - -def handle_set_pkg_owner_global(goptions, session, args): - "[admin] Set the owner for a package globally" - usage = "usage: %prog set-pkg-owner-global [options] [ ...]" - parser = OptionParser(usage=get_usage_str(usage)) - parser.add_option("--verbose", action='store_true', help="List changes") - parser.add_option("--test", action='store_true', help="Test mode") - parser.add_option("--old-user", "--from", action="store", - help="Only change ownership for packages belonging to this user") - (options, args) = parser.parse_args(args) - if options.old_user: - if len(args) < 1: - parser.error("Please specify an owner") - elif len(args) < 2: - parser.error("Please specify an owner and at least one package") - activate_session(session, goptions) - owner = args[0] - packages = args[1:] - user = session.getUser(owner) - if not user: - error("No such user: %s" % owner) - opts = {'with_dups': True} - old_user = None - if options.old_user: - old_user = session.getUser(options.old_user) - if not old_user: - error("No such user: %s" % options.old_user) - opts['userID'] = old_user['id'] - to_change = [] - for package in packages: - entries = session.listPackages(pkgID=package, **opts) - if not entries: - print("No data for package %s" % package) - continue - to_change.extend(entries) - if not packages and options.old_user: - entries = session.listPackages(**opts) - if not entries: - error("No data for user %s" % old_user['name']) - to_change.extend(entries) - for entry in to_change: - if user['id'] == entry['owner_id']: - if options.verbose: - print("Preserving owner=%s for package %s in tag %s" - % (user['name'], package, entry['tag_name'])) - else: - if options.test: - print("Would have changed owner for %s in tag %s: %s -> %s" - % (entry['package_name'], entry['tag_name'], entry['owner_name'], - user['name'])) - continue - if options.verbose: - print("Changing owner for %s in tag %s: %s -> %s" - % (entry['package_name'], entry['tag_name'], entry['owner_name'], - user['name'])) - session.packageListSetOwner(entry['tag_id'], entry['package_name'], user['id']) - - diff --git a/cli/koji_cli/commands/cmd_set_task_priority.py b/cli/koji_cli/commands/cmd_set_task_priority.py deleted file mode 100644 index 990afd1..0000000 --- a/cli/koji_cli/commands/cmd_set_task_priority.py +++ /dev/null @@ -1,47 +0,0 @@ -from __future__ import absolute_import, division - -from optparse import OptionParser - - -import koji - -from koji_cli.lib import ( - activate_session, - error, - get_usage_str, - warn -) - - -def handle_set_task_priority(goptions, session, args): - "[admin] Set task priority" - usage = "usage: %prog set-task-priority [options] --priority= " \ - "[ ...]" - parser = OptionParser(usage=get_usage_str(usage)) - parser.add_option("--priority", type="int", help="New priority") - parser.add_option("--recurse", action="store_true", default=False, - help="Change priority of child tasks as well") - (options, args) = parser.parse_args(args) - if len(args) == 0: - parser.error("You must specify at least one task id") - - if options.priority is None: - parser.error("You must specify --priority") - try: - tasks = [int(a) for a in args] - except ValueError: - parser.error("Task numbers must be integers") - - activate_session(session, goptions) - - if not session.hasPerm('admin'): - logged_user = session.getLoggedInUser() - error("admin permission required (logged in as %s)" % logged_user['name']) - - for task_id in tasks: - try: - session.setTaskPriority(task_id, options.priority, options.recurse) - except koji.GenericError: - warn("Can't update task priority on closed task: %s" % task_id) - - diff --git a/cli/koji_cli/commands/cmd_show_groups.py b/cli/koji_cli/commands/cmd_show_groups.py deleted file mode 100644 index 4039457..0000000 --- a/cli/koji_cli/commands/cmd_show_groups.py +++ /dev/null @@ -1,43 +0,0 @@ -from __future__ import absolute_import, division - -import pprint -from optparse import OptionParser - - -import koji - -from koji_cli.lib import ( - ensure_connection, - get_usage_str -) - - -def anon_handle_show_groups(goptions, session, args): - "[info] Show groups data for a tag" - usage = "usage: %prog show-groups [options] " - parser = OptionParser(usage=get_usage_str(usage)) - parser.add_option("--comps", action="store_true", help="Print in comps format") - parser.add_option("-x", "--expand", action="store_true", default=False, - help="Expand groups in comps format") - parser.add_option("--spec", action="store_true", help="Print build spec") - parser.add_option("--show-blocked", action="store_true", dest="incl_blocked", - help="Show blocked packages") - (options, args) = parser.parse_args(args) - if len(args) != 1: - parser.error("Incorrect number of arguments") - if options.incl_blocked and (options.comps or options.spec): - parser.error("--show-blocked doesn't make sense for comps/spec output") - ensure_connection(session, goptions) - tag = args[0] - callopts = {} - if options.incl_blocked: - callopts['incl_blocked'] = True - groups = session.getTagGroups(tag, **callopts) - if options.comps: - print(koji.generate_comps(groups, expand_groups=options.expand)) - elif options.spec: - print(koji.make_groups_spec(groups, name='buildgroups', buildgroup='build')) - else: - pprint.pprint(groups) - - diff --git a/cli/koji_cli/commands/cmd_spin_appliance.py b/cli/koji_cli/commands/cmd_spin_appliance.py deleted file mode 100644 index 2337f28..0000000 --- a/cli/koji_cli/commands/cmd_spin_appliance.py +++ /dev/null @@ -1,57 +0,0 @@ -from __future__ import absolute_import, division - -from optparse import OptionParser - - - -from koji_cli.lib import ( - get_usage_str -) - - -def handle_spin_appliance(options, session, args): - """[build] Create an appliance given a kickstart file""" - - # Usage & option parsing - usage = "usage: %prog spin-appliance [options] " \ - "" - parser = OptionParser(usage=get_usage_str(usage)) - parser.add_option("--wait", action="store_true", - help="Wait on the appliance creation, even if running in the background") - parser.add_option("--nowait", action="store_false", dest="wait", - help="Don't wait on appliance creation") - parser.add_option("--noprogress", action="store_true", - help="Do not display progress of the upload") - parser.add_option("--background", action="store_true", - help="Run the appliance creation task at a lower priority") - parser.add_option("--ksurl", metavar="SCMURL", - help="The URL to the SCM containing the kickstart file") - parser.add_option("--ksversion", metavar="VERSION", - help="The syntax version used in the kickstart file") - parser.add_option("--scratch", action="store_true", help="Create a scratch appliance") - parser.add_option("--repo", action="append", - help="Specify a repo that will override the repo used to install " - "RPMs in the appliance. May be used multiple times. The " - "build tag repo associated with the target is the default.") - parser.add_option("--release", help="Forcibly set the release field") - parser.add_option("--specfile", metavar="URL", - help="SCM URL to spec file fragment to use to generate wrapper RPMs") - parser.add_option("--skip-tag", action="store_true", help="Do not attempt to tag package") - parser.add_option("--vmem", metavar="VMEM", default=None, - help="Set the amount of virtual memory in the appliance in MB, " - "default is 512") - parser.add_option("--vcpu", metavar="VCPU", default=None, - help="Set the number of virtual cpus in the appliance, default is 1") - parser.add_option("--format", metavar="DISK_FORMAT", default='raw', - help="Disk format, default is raw. Other options are qcow, qcow2, and vmx.") - - (task_options, args) = parser.parse_args(args) - - # Make sure the target and kickstart is specified. - print('spin-appliance is deprecated and will be replaced with image-build') - if len(args) != 5: - parser.error("Five arguments are required: a name, a version, an architecture, " - "a build target, and a relative path to a kickstart file.") - return _build_image(options, task_options, session, args, 'appliance') - - diff --git a/cli/koji_cli/commands/cmd_spin_livecd.py b/cli/koji_cli/commands/cmd_spin_livecd.py deleted file mode 100644 index ffc03d6..0000000 --- a/cli/koji_cli/commands/cmd_spin_livecd.py +++ /dev/null @@ -1,54 +0,0 @@ -from __future__ import absolute_import, division - -from optparse import OptionParser - - - -from koji_cli.lib import ( - get_usage_str -) - - -def handle_spin_livecd(options, session, args): - """[build] Create a live CD image given a kickstart file""" - - # Usage & option parsing. - usage = "usage: %prog spin-livecd [options] " - parser = OptionParser(usage=get_usage_str(usage)) - parser.add_option("--wait", action="store_true", - help="Wait on the livecd creation, even if running in the background") - parser.add_option("--nowait", action="store_false", dest="wait", - help="Don't wait on livecd creation") - parser.add_option("--noprogress", action="store_true", - help="Do not display progress of the upload") - parser.add_option("--background", action="store_true", - help="Run the livecd creation task at a lower priority") - parser.add_option("--ksurl", metavar="SCMURL", - help="The URL to the SCM containing the kickstart file") - parser.add_option("--ksversion", metavar="VERSION", - help="The syntax version used in the kickstart file") - parser.add_option("--scratch", action="store_true", - help="Create a scratch LiveCD image") - parser.add_option("--repo", action="append", - help="Specify a repo that will override the repo used to install " - "RPMs in the LiveCD. May be used multiple times. The " - "build tag repo associated with the target is the default.") - parser.add_option("--release", help="Forcibly set the release field") - parser.add_option("--volid", help="Set the volume id") - parser.add_option("--specfile", metavar="URL", - help="SCM URL to spec file fragment to use to generate wrapper RPMs") - parser.add_option("--skip-tag", action="store_true", - help="Do not attempt to tag package") - (task_options, args) = parser.parse_args(args) - - # Make sure the target and kickstart is specified. - print('spin-livecd is deprecated and will be replaced with spin-livemedia') - if len(args) != 5: - parser.error("Five arguments are required: a name, a version, an architecture, " - "a build target, and a relative path to a kickstart file.") - if task_options.volid is not None and len(task_options.volid) > 32: - parser.error('Volume ID has a maximum length of 32 characters') - return _build_image(options, task_options, session, args, 'livecd') - - -# This handler is for spinning livemedia images diff --git a/cli/koji_cli/commands/cmd_spin_livemedia.py b/cli/koji_cli/commands/cmd_spin_livemedia.py deleted file mode 100644 index 0f0e63e..0000000 --- a/cli/koji_cli/commands/cmd_spin_livemedia.py +++ /dev/null @@ -1,75 +0,0 @@ -from __future__ import absolute_import, division - -from optparse import OptionParser - - - -from koji_cli.lib import ( - get_usage_str -) - - -def handle_spin_livemedia(options, session, args): - """[build] Create a livemedia image given a kickstart file""" - - # Usage & option parsing. - usage = "usage: %prog spin-livemedia [options] " \ - "" - parser = OptionParser(usage=get_usage_str(usage)) - parser.add_option("--wait", action="store_true", - help="Wait on the livemedia creation, even if running in the background") - parser.add_option("--nowait", action="store_false", dest="wait", - help="Don't wait on livemedia creation") - parser.add_option("--noprogress", action="store_true", - help="Do not display progress of the upload") - parser.add_option("--background", action="store_true", - help="Run the livemedia creation task at a lower priority") - parser.add_option("--ksurl", metavar="SCMURL", - help="The URL to the SCM containing the kickstart file") - parser.add_option("--install-tree-url", metavar="URL", - help="Provide the URL for the install tree") - parser.add_option("--ksversion", metavar="VERSION", - help="The syntax version used in the kickstart file") - parser.add_option("--scratch", action="store_true", - help="Create a scratch LiveMedia image") - parser.add_option("--repo", action="append", - help="Specify a repo that will override the repo used to install " - "RPMs in the LiveMedia. May be used multiple times. The " - "build tag repo associated with the target is the default.") - parser.add_option("--release", help="Forcibly set the release field") - parser.add_option("--volid", help="Set the volume id") - parser.add_option("--specfile", metavar="URL", - help="SCM URL to spec file fragment to use to generate wrapper RPMs") - parser.add_option("--skip-tag", action="store_true", help="Do not attempt to tag package") - parser.add_option("--can-fail", action="store", dest="optional_arches", - metavar="ARCH1,ARCH2,...", default="", - help="List of archs which are not blocking for build (separated by commas.") - parser.add_option('--lorax_dir', metavar='DIR', - help='The relative path to the lorax templates ' - 'directory within the checkout of "lorax_url".') - parser.add_option('--lorax_url', metavar='URL', - help='The URL to the SCM containing any custom lorax templates that are ' - 'to be used to override the default templates.') - parser.add_option('--nomacboot', action="store_true", - help="Pass the nomacboot option to livemedia-creator") - parser.add_option('--ksrepo', action="store_true", - help="Do not overwrite repos in the kickstart") - parser.add_option('--squashfs-only', action="store_true", - help="Use a plain squashfs filesystem.") - parser.add_option('--compress-arg', action="append", default=[], metavar="ARG OPT", - help="List of compressions.") - (task_options, args) = parser.parse_args(args) - - # Make sure the target and kickstart is specified. - if len(args) != 5: - parser.error("Five arguments are required: a name, a version, a build target, " - "an architecture, and a relative path to a kickstart file.") - if task_options.lorax_url is not None and task_options.lorax_dir is None: - parser.error('The "--lorax_url" option requires that "--lorax_dir" also be used.') - if task_options.volid is not None and len(task_options.volid) > 32: - parser.error('Volume ID has a maximum length of 32 characters') - return _build_image(options, task_options, session, args, 'livemedia') - - -# This handler is for spinning appliance images -# diff --git a/cli/koji_cli/commands/cmd_tag_build.py b/cli/koji_cli/commands/cmd_tag_build.py deleted file mode 100644 index c888a00..0000000 --- a/cli/koji_cli/commands/cmd_tag_build.py +++ /dev/null @@ -1,40 +0,0 @@ -from __future__ import absolute_import, division - -from optparse import OptionParser - - - -from koji_cli.lib import ( - _running_in_bg, - activate_session, - get_usage_str, - watch_tasks -) - - -def handle_tag_build(opts, session, args): - "[bind] Apply a tag to one or more builds" - usage = "usage: %prog tag-build [options] [ ...]" - parser = OptionParser(usage=get_usage_str(usage)) - parser.add_option("--force", action="store_true", help="force operation") - parser.add_option("--wait", action="store_true", - help="Wait on task, even if running in the background") - parser.add_option("--nowait", action="store_false", dest="wait", help="Do not wait on task") - (options, args) = parser.parse_args(args) - if len(args) < 2: - parser.error( - "This command takes at least two arguments: a tag name/ID and one or more package " - "n-v-r's") - activate_session(session, opts) - tasks = [] - for pkg in args[1:]: - task_id = session.tagBuild(args[0], pkg, force=options.force) - # XXX - wait on task - tasks.append(task_id) - print("Created task %d" % task_id) - if options.wait or (options.wait is None and not _running_in_bg()): - session.logout() - return watch_tasks(session, tasks, quiet=opts.quiet, - poll_interval=opts.poll_interval, topurl=opts.topurl) - - diff --git a/cli/koji_cli/commands/cmd_taginfo.py b/cli/koji_cli/commands/cmd_taginfo.py deleted file mode 100644 index 8a9adab..0000000 --- a/cli/koji_cli/commands/cmd_taginfo.py +++ /dev/null @@ -1,119 +0,0 @@ -from __future__ import absolute_import, division - -import pprint -import time -from optparse import OptionParser - - -import koji - -from koji_cli.lib import ( - ensure_connection, - format_inheritance_flags, - get_usage_str -) - - -def anon_handle_taginfo(goptions, session, args): - "[info] Print basic information about a tag" - usage = "usage: %prog taginfo [options] [ ...]" - parser = OptionParser(usage=get_usage_str(usage)) - parser.add_option("--event", type='int', metavar="EVENT#", help="query at event") - parser.add_option("--ts", type='int', metavar="TIMESTAMP", - help="query at last event before timestamp") - parser.add_option("--repo", type='int', metavar="REPO#", help="query at event for a repo") - (options, args) = parser.parse_args(args) - if len(args) < 1: - parser.error("Please specify a tag") - ensure_connection(session, goptions) - event = koji.util.eventFromOpts(session, options) - event_opts = {} - if event: - event['timestr'] = time.asctime(time.localtime(event['ts'])) - print("Querying at event %(id)i (%(timestr)s)" % event) - event_opts['event'] = event['id'] - perms = dict([(p['id'], p['name']) for p in session.getAllPerms()]) - - tags = [] - for tag in args: - info = session.getBuildConfig(tag, **event_opts) - if info is None: - try: - info = session.getBuildConfig(int(tag), **event_opts) - except ValueError: - info = None - if info is None: - parser.error('No such tag: %s' % tag) - tags.append(info) - - for n, info in enumerate(tags): - if n > 0: - print('') - print("Tag: %(name)s [%(id)d]" % info) - print("Arches: %(arches)s" % info) - group_list = sorted([x['name'] for x in session.getTagGroups(info['id'], **event_opts)]) - print("Groups: " + ', '.join(group_list)) - if info.get('locked'): - print('LOCKED') - if info.get('perm_id') is not None: - perm_id = info['perm_id'] - print("Required permission: %r" % perms.get(perm_id, perm_id)) - if session.mavenEnabled(): - print("Maven support?: %s" % (info['maven_support'] and 'yes' or 'no')) - print("Include all Maven archives?: %s" % - (info['maven_include_all'] and 'yes' or 'no')) - if 'extra' in info: - print("Tag options:") - for key in sorted(info['extra'].keys()): - line = " %s : %s" % (key, pprint.pformat(info['extra'][key])) - if key in info.get('config_inheritance', {}).get('extra', []): - line = "%-30s [%s]" % (line, info['config_inheritance']['extra'][key]['name']) - print(line) - dest_targets = session.getBuildTargets(destTagID=info['id'], **event_opts) - build_targets = session.getBuildTargets(buildTagID=info['id'], **event_opts) - repos = {} - if not event: - for target in dest_targets + build_targets: - if target['build_tag'] not in repos: - repo = session.getRepo(target['build_tag']) - if repo is None: - repos[target['build_tag']] = "no active repo" - else: - repos[target['build_tag']] = "repo#%(id)i: %(creation_time)s" % repo - if dest_targets: - print("Targets that build into this tag:") - for target in dest_targets: - if event: - print(" %s (%s)" % (target['name'], target['build_tag_name'])) - else: - print(" %s (%s, %s)" % - (target['name'], target['build_tag_name'], repos[target['build_tag']])) - if build_targets: - print("This tag is a buildroot for one or more targets") - if not event: - print("Current repo: %s" % repos[info['id']]) - print("Targets that build from this tag:") - for target in build_targets: - print(" %s" % target['name']) - external_repos = session.getTagExternalRepos(tag_info=info['id'], **event_opts) - if external_repos: - print("External repos:") - for rinfo in external_repos: - if 'arches' not in rinfo: - # older hubs will not return this field - rinfo['arches'] = '-' - elif not rinfo['arches']: - rinfo['arches'] = 'inherited from tag' - # TODO else intersection of arches? - print(" %(priority)3i %(external_repo_name)s " - "(%(url)s, merge mode: %(merge_mode)s), arches: %(arches)s" % rinfo) - print("Inheritance:") - for parent in session.getInheritanceData(info['id'], **event_opts): - parent['flags'] = format_inheritance_flags(parent) - print(" %(priority)-4d %(flags)s %(name)s [%(parent_id)s]" % parent) - if parent['maxdepth'] is not None: - print(" maxdepth: %(maxdepth)s" % parent) - if parent['pkg_filter']: - print(" package filter: %(pkg_filter)s" % parent) - - diff --git a/cli/koji_cli/commands/cmd_taskinfo.py b/cli/koji_cli/commands/cmd_taskinfo.py deleted file mode 100644 index dcfb4cb..0000000 --- a/cli/koji_cli/commands/cmd_taskinfo.py +++ /dev/null @@ -1,30 +0,0 @@ -from __future__ import absolute_import, division - -from optparse import OptionParser - - - -from koji_cli.lib import ( - ensure_connection, - get_usage_str -) - - -def anon_handle_taskinfo(goptions, session, args): - """[info] Show information about a task""" - usage = "usage: %prog taskinfo [options] [ ...]" - parser = OptionParser(usage=get_usage_str(usage)) - parser.add_option("-r", "--recurse", action="store_true", - help="Show children of this task as well") - parser.add_option("-v", "--verbose", action="store_true", help="Be verbose") - (options, args) = parser.parse_args(args) - if len(args) < 1: - parser.error("You must specify at least one task ID") - - ensure_connection(session, goptions) - - for arg in args: - task_id = int(arg) - _printTaskInfo(session, task_id, goptions.topdir, 0, options.recurse, options.verbose) - - diff --git a/cli/koji_cli/commands/cmd_unblock_group_pkg.py b/cli/koji_cli/commands/cmd_unblock_group_pkg.py deleted file mode 100644 index 5b56a2c..0000000 --- a/cli/koji_cli/commands/cmd_unblock_group_pkg.py +++ /dev/null @@ -1,26 +0,0 @@ -from __future__ import absolute_import, division - -from optparse import OptionParser - - - -from koji_cli.lib import ( - activate_session, - get_usage_str -) - - -def handle_unblock_group_pkg(goptions, session, args): - "[admin] Unblock a package from a group's package listing" - usage = "usage: %prog unblock-group-pkg [options] [ ...]" - parser = OptionParser(usage=get_usage_str(usage)) - (options, args) = parser.parse_args(args) - if len(args) < 3: - parser.error("You must specify a tag name, group name, and one or more package names") - tag = args[0] - group = args[1] - activate_session(session, goptions) - for pkg in args[2:]: - session.groupPackageListUnblock(tag, group, pkg) - - diff --git a/cli/koji_cli/commands/cmd_unblock_group_req.py b/cli/koji_cli/commands/cmd_unblock_group_req.py deleted file mode 100644 index 8703cfd..0000000 --- a/cli/koji_cli/commands/cmd_unblock_group_req.py +++ /dev/null @@ -1,26 +0,0 @@ -from __future__ import absolute_import, division - -from optparse import OptionParser - - - -from koji_cli.lib import ( - activate_session, - get_usage_str -) - - -def handle_unblock_group_req(goptions, session, args): - "[admin] Unblock a group's requirement listing" - usage = "usage: %prog unblock-group-req [options] " - parser = OptionParser(usage=get_usage_str(usage)) - (options, args) = parser.parse_args(args) - if len(args) != 3: - parser.error("You must specify a tag name and two group names") - tag = args[0] - group = args[1] - req = args[2] - activate_session(session, goptions) - session.groupReqListUnblock(tag, group, req) - - diff --git a/cli/koji_cli/commands/cmd_unblock_notification.py b/cli/koji_cli/commands/cmd_unblock_notification.py deleted file mode 100644 index e344a1b..0000000 --- a/cli/koji_cli/commands/cmd_unblock_notification.py +++ /dev/null @@ -1,34 +0,0 @@ -from __future__ import absolute_import, division - -from optparse import OptionParser - - - -from koji_cli.lib import ( - activate_session, - get_usage_str -) - - -def handle_unblock_notification(goptions, session, args): - "[monitor] Unblock user's notification" - usage = "usage: %prog unblock-notification [options] [ ...]" - parser = OptionParser(usage=get_usage_str(usage)) - (options, args) = parser.parse_args(args) - - activate_session(session, goptions) - - if len(args) < 1: - parser.error("At least one notification block id has to be specified") - - try: - n_ids = [int(x) for x in args] - except ValueError: - parser.error("All notification block ids has to be integers") - - for n_id in n_ids: - session.deleteNotificationBlock(n_id) - if not goptions.quiet: - print("Notification block %d successfully removed." % n_id) - - diff --git a/cli/koji_cli/commands/cmd_unblock_pkg.py b/cli/koji_cli/commands/cmd_unblock_pkg.py deleted file mode 100644 index 504293e..0000000 --- a/cli/koji_cli/commands/cmd_unblock_pkg.py +++ /dev/null @@ -1,26 +0,0 @@ -from __future__ import absolute_import, division - -from optparse import OptionParser - - - -from koji_cli.lib import ( - activate_session, - get_usage_str -) - - -def handle_unblock_pkg(goptions, session, args): - "[admin] Unblock a package in the listing for tag" - usage = "usage: %prog unblock-pkg [options] [ ...]" - parser = OptionParser(usage=get_usage_str(usage)) - (options, args) = parser.parse_args(args) - if len(args) < 2: - parser.error("Please specify a tag and at least one package") - activate_session(session, goptions) - tag = args[0] - with session.multicall(strict=True) as m: - for package in args[1:]: - m.packageListUnblock(tag, package) - - diff --git a/cli/koji_cli/commands/cmd_unlock_tag.py b/cli/koji_cli/commands/cmd_unlock_tag.py deleted file mode 100644 index da13ed3..0000000 --- a/cli/koji_cli/commands/cmd_unlock_tag.py +++ /dev/null @@ -1,54 +0,0 @@ -from __future__ import absolute_import, division - -import fnmatch -from optparse import OptionParser - - - -from koji_cli.lib import ( - activate_session, - get_usage_str -) - - -def handle_unlock_tag(goptions, session, args): - "[admin] Unlock a tag" - usage = "usage: %prog unlock-tag [options] [ ...]" - parser = OptionParser(usage=get_usage_str(usage)) - parser.add_option("--glob", action="store_true", help="Treat args as glob patterns") - parser.add_option("-n", "--test", action="store_true", help="Test mode") - (options, args) = parser.parse_args(args) - if len(args) < 1: - parser.error("Please specify a tag") - activate_session(session, goptions) - if options.glob: - selected = [] - for tag in session.listTags(): - for pattern in args: - if fnmatch.fnmatch(tag['name'], pattern): - selected.append(tag) - break - if not selected: - print("No tags matched") - else: - selected = [] - for name in args: - tag = session.getTag(name) - if tag is None: - parser.error("No such tag: %s" % name) - selected.append(tag) - for tag in selected: - opts = {} - if tag['locked']: - opts['locked'] = False - if tag['perm_id']: - opts['perm'] = None - if not opts: - print("Tag %(name)s: not locked" % tag) - continue - if options.test: - print("Tag %s: skipping changes: %r" % (tag['name'], opts)) - else: - session.editTag2(tag['id'], **opts) - - diff --git a/cli/koji_cli/commands/cmd_untag_build.py b/cli/koji_cli/commands/cmd_untag_build.py deleted file mode 100644 index def3624..0000000 --- a/cli/koji_cli/commands/cmd_untag_build.py +++ /dev/null @@ -1,113 +0,0 @@ -from __future__ import absolute_import, division - -import itertools -from optparse import OptionParser - - -import koji - -from koji_cli.lib import ( - activate_session, - error, - get_usage_str, - warn -) - - -def handle_untag_build(goptions, session, args): - "[bind] Remove a tag from one or more builds" - usage = "usage: %prog untag-build [options] [ ...]" - parser = OptionParser(usage=get_usage_str(usage)) - parser.add_option("--all", action="store_true", - help="untag all versions of the package in this tag, pkg is package name") - parser.add_option("--non-latest", action="store_true", - help="untag all versions of the package in this tag except the latest, " - "pkg is package name") - parser.add_option("-n", "--test", action="store_true", help="test mode") - parser.add_option("-v", "--verbose", action="store_true", help="print details") - parser.add_option("--force", action="store_true", help="force operation") - (options, args) = parser.parse_args(args) - if options.non_latest and options.force: - if len(args) < 1: - parser.error("Please specify a tag") - elif len(args) < 2: - parser.error( - "This command takes at least two arguments: a tag name/ID and one or more package " - "n-v-r's or package names") - activate_session(session, goptions) - tag = session.getTag(args[0]) - if not tag: - parser.error("No such tag: %s" % args[0]) - if options.all: - result = [] - with session.multicall() as m: - result.extend([m.queryHistory(tag=args[0], package=pkg, active=True) - for pkg in args[1:]]) - builds = [] - for r in result: - builds.extend(r.result['tag_listing']) - elif options.non_latest: - if options.force and len(args) == 1: - tagged = session.queryHistory(tag=args[0], active=True)['tag_listing'] - tagged = sorted(tagged, key=lambda k: (k['create_event']), reverse=True) - else: - result = [] - with session.multicall() as m: - result.extend([m.queryHistory(tag=args[0], package=pkg, active=True) - for pkg in args[1:]]) - tagged = [] - for r in result: - tagged.extend(r.result['tag_listing']) - tagged = sorted(tagged, key=lambda k: (k['create_event']), reverse=True) - # listTagged orders entries latest first - seen_pkg = {} - builds = [] - for binfo in tagged: - if binfo['name'] not in seen_pkg: - # latest for this package - nvr = '%s-%s-%s' % (binfo['name'], binfo['version'], binfo['release']) - if options.verbose: - print("Leaving latest build for package %s: %s" % (binfo['name'], nvr)) - else: - builds.append(binfo) - seen_pkg[binfo['name']] = 1 - else: - # find all pkg's builds in tag - pkgs = set([koji.parse_NVR(nvr)['name'] for nvr in args[1:]]) - result = [] - with session.multicall() as m: - result.extend([m.queryHistory(tag=args[0], pkg=pkg, active=True) for pkg in pkgs]) - tagged = [] - for r in result: - tagged.append(r.result['tag_listing']) - # flatten - tagged = list(itertools.chain(*[t for t in tagged])) - idx = dict([('%s-%s-%s' % (b['name'], b['version'], b['release']), b) for b in tagged]) - - # check exact builds - builds = [] - for nvr in args[1:]: - binfo = idx.get(nvr) - if binfo: - builds.append(binfo) - else: - # not in tag, see if it even exists - binfo = session.getBuild(nvr) - if not binfo: - warn("No such build: %s" % nvr) - else: - warn("Build %s not in tag %s" % (nvr, tag['name'])) - if not options.force: - error() - builds.reverse() - with session.multicall(strict=True) as m: - for binfo in builds: - build_nvr = '%s-%s-%s' % (binfo['name'], binfo['version'], binfo['release']) - if options.test: - print("would have untagged %s" % build_nvr) - else: - if options.verbose: - print("untagging %s" % build_nvr) - m.untagBuild(tag['name'], build_nvr, force=options.force) - - diff --git a/cli/koji_cli/commands/cmd_userinfo.py b/cli/koji_cli/commands/cmd_userinfo.py deleted file mode 100644 index 8565b66..0000000 --- a/cli/koji_cli/commands/cmd_userinfo.py +++ /dev/null @@ -1,67 +0,0 @@ -from __future__ import absolute_import, division - -from optparse import OptionParser - -from six.moves import filter, zip - -import koji - -from koji_cli.lib import ( - ensure_connection, - get_usage_str, - warn -) - - -def anon_handle_userinfo(goptions, session, args): - """[admin] Show information about a user""" - usage = "usage: %prog userinfo [options] [ ...]" - parser = OptionParser(usage=get_usage_str(usage)) - (options, args) = parser.parse_args(args) - if len(args) < 1: - parser.error("You must specify at least one username") - - ensure_connection(session, goptions) - - with session.multicall() as m: - userinfos = [m.getUser(user) for user in args] - user_infos = [] - for username, userinfo in zip(args, userinfos): - if userinfo.result is None: - warn("No such user: %s\n" % username) - continue - user_infos.append(userinfo.result) - user_infos = list(filter(None, user_infos)) - - calls = [] - with session.multicall() as m: - for user in user_infos: - results = [] - if not user: - warn("No such user: %s\n" % user) - continue - results.append(m.getUserPerms(user['id'])) - results.append(m.listPackages(userID=user['id'], with_dups=True, - queryOpts={'countOnly': True})) - results.append(m.listTasks(opts={'owner': user['id'], 'parent': None}, - queryOpts={'countOnly': True})) - results.append(m.listBuilds(userID=user['id'], queryOpts={'countOnly': True})) - calls.append(results) - - for userinfo, (perms, pkgs, tasks, builds) in zip(user_infos, calls): - print("User name: %s" % userinfo['name']) - print("User ID: %d" % userinfo['id']) - if 'krb_principals' in userinfo: - print("krb principals:") - for krb in userinfo['krb_principals']: - print(" %s" % krb) - if perms.result: - print("Permissions:") - for perm in perms.result: - print(" %s" % perm) - print("Status: %s" % koji.USER_STATUS[userinfo['status']]) - print("Usertype: %s" % koji.USERTYPES[userinfo['usertype']]) - print("Number of packages: %d" % pkgs.result) - print("Number of tasks: %d" % tasks.result) - print("Number of builds: %d" % builds.result) - print('') diff --git a/cli/koji_cli/commands/cmd_version.py b/cli/koji_cli/commands/cmd_version.py deleted file mode 100644 index 926a5f5..0000000 --- a/cli/koji_cli/commands/cmd_version.py +++ /dev/null @@ -1,22 +0,0 @@ -from __future__ import absolute_import, division - - - -import koji - -from koji_cli.lib import ( - ensure_connection -) - - -def handle_version(goptions, session, args): - """Report client and hub versions""" - ensure_connection(session, goptions) - print('Client: %s' % koji.__version__) - try: - version = session.getKojiVersion() - print("Hub: %s" % version) - except koji.GenericError: - print("Hub: Can't determine (older than 1.23)") - - diff --git a/cli/koji_cli/commands/cmd_wait_repo.py b/cli/koji_cli/commands/cmd_wait_repo.py deleted file mode 100644 index d4b4784..0000000 --- a/cli/koji_cli/commands/cmd_wait_repo.py +++ /dev/null @@ -1,110 +0,0 @@ -from __future__ import absolute_import, division - -import time -from optparse import OptionParser - - -import koji - -from koji_cli.lib import ( - ensure_connection, - error, - get_usage_str, - warn -) - - -def anon_handle_wait_repo(options, session, args): - "[monitor] Wait for a repo to be regenerated" - usage = "usage: %prog wait-repo [options] " - parser = OptionParser(usage=get_usage_str(usage)) - parser.add_option("--build", metavar="NVR", dest="builds", action="append", default=[], - help="Check that the given build is in the newly-generated repo " - "(may be used multiple times)") - parser.add_option("--target", action="store_true", - help="Interpret the argument as a build target name") - parser.add_option("--timeout", type="int", default=120, - help="Amount of time to wait (in minutes) before giving up " - "(default: 120)") - parser.add_option("--quiet", action="store_true", default=options.quiet, - help="Suppress output, success or failure will be indicated by the return " - "value only") - (suboptions, args) = parser.parse_args(args) - - start = time.time() - - builds = [koji.parse_NVR(build) for build in suboptions.builds] - if len(args) < 1: - parser.error("Please specify a tag name") - elif len(args) > 1: - parser.error("Only one tag may be specified") - - tag = args[0] - - ensure_connection(session, options) - if suboptions.target: - target_info = session.getBuildTarget(tag) - if not target_info: - parser.error("No such build target: %s" % tag) - tag = target_info['build_tag_name'] - tag_id = target_info['build_tag'] - else: - tag_info = session.getTag(tag) - if not tag_info: - parser.error("No such tag: %s" % tag) - targets = session.getBuildTargets(buildTagID=tag_info['id']) - if not targets: - warn("%(name)s is not a build tag for any target" % tag_info) - targets = session.getBuildTargets(destTagID=tag_info['id']) - if targets: - maybe = {}.fromkeys([t['build_tag_name'] for t in targets]) - maybe = sorted(maybe.keys()) - warn("Suggested tags: %s" % ', '.join(maybe)) - error() - tag_id = tag_info['id'] - - for nvr in builds: - data = session.getLatestBuilds(tag_id, package=nvr["name"]) - if len(data) == 0: - warn("Package %s is not in tag %s" % (nvr["name"], tag)) - else: - present_nvr = [x["nvr"] for x in data][0] - expected_nvr = '%(name)s-%(version)s-%(release)s' % nvr - if present_nvr != expected_nvr: - warn("nvr %s is not current in tag %s\n latest build in %s is %s" % - (expected_nvr, tag, tag, present_nvr)) - - last_repo = None - repo = session.getRepo(tag_id) - - while True: - if builds and repo and repo != last_repo: - if koji.util.checkForBuilds(session, tag_id, builds, repo['create_event'], - latest=True): - if not suboptions.quiet: - print("Successfully waited %s for %s to appear in the %s repo" % - (koji.util.duration(start), koji.util.printList(suboptions.builds), tag)) - return - - if (time.time() - start) >= (suboptions.timeout * 60.0): - if not suboptions.quiet: - if builds: - error("Unsuccessfully waited %s for %s to appear in the %s repo" % - (koji.util.duration(start), koji.util.printList(suboptions.builds), tag)) - else: - error("Unsuccessfully waited %s for a new %s repo" % - (koji.util.duration(start), tag)) - error() - - time.sleep(options.poll_interval) - last_repo = repo - repo = session.getRepo(tag_id) - - if not builds: - if repo != last_repo: - if not suboptions.quiet: - print("Successfully waited %s for a new %s repo" % - (koji.util.duration(start), tag)) - return - - diff --git a/cli/koji_cli/commands/cmd_watch_logs.py b/cli/koji_cli/commands/cmd_watch_logs.py deleted file mode 100644 index 10189a4..0000000 --- a/cli/koji_cli/commands/cmd_watch_logs.py +++ /dev/null @@ -1,49 +0,0 @@ -from __future__ import absolute_import, division - -from optparse import OptionParser - - - -from koji_cli.lib import ( - _list_tasks, - activate_session, - ensure_connection, - get_usage_str, - watch_logs -) - - -def anon_handle_watch_logs(goptions, session, args): - "[monitor] Watch logs in realtime" - usage = "usage: %prog watch-logs [options] [ ...]" - parser = OptionParser(usage=get_usage_str(usage)) - parser.add_option("--log", help="Watch only a specific log") - parser.add_option("--mine", action="store_true", - help="Watch logs for all your tasks, task_id arguments are forbidden in " - "this case.") - parser.add_option("--follow", action="store_true", help="Follow spawned child tasks") - (options, args) = parser.parse_args(args) - - if options.mine: - activate_session(session, goptions) - if args: - parser.error("Selection options cannot be combined with a task list") - tasks = _list_tasks(options, session) - tasks = [t['id'] for t in tasks] - if not tasks: - print("You've no active tasks.") - return - else: - ensure_connection(session, goptions) - tasks = [] - for task in args: - try: - tasks.append(int(task)) - except ValueError: - parser.error("task id must be an integer") - if not tasks: - parser.error("at least one task id must be specified") - - watch_logs(session, tasks, options, goptions.poll_interval) - - diff --git a/cli/koji_cli/commands/cmd_watch_task.py b/cli/koji_cli/commands/cmd_watch_task.py deleted file mode 100644 index ae6ea81..0000000 --- a/cli/koji_cli/commands/cmd_watch_task.py +++ /dev/null @@ -1,60 +0,0 @@ -from __future__ import absolute_import, division - -from optparse import OptionParser - - - -from koji_cli.lib import ( - _list_tasks, - activate_session, - ensure_connection, - get_usage_str, - watch_tasks -) - - -def anon_handle_watch_task(goptions, session, args): - "[monitor] Track progress of particular tasks" - usage = "usage: %prog watch-task [options] [ ...]" - parser = OptionParser(usage=get_usage_str(usage)) - parser.add_option("--quiet", action="store_true", default=goptions.quiet, - help="Do not print the task information") - parser.add_option("--mine", action="store_true", help="Just watch your tasks") - parser.add_option("--user", help="Only tasks for this user") - parser.add_option("--arch", help="Only tasks for this architecture") - parser.add_option("--method", help="Only tasks of this method") - parser.add_option("--channel", help="Only tasks in this channel") - parser.add_option("--host", help="Only tasks for this host") - (options, args) = parser.parse_args(args) - selection = (options.mine or - options.user or - options.arch or - options.method or - options.channel or - options.host) - if args and selection: - parser.error("Selection options cannot be combined with a task list") - - if options.mine: - activate_session(session, goptions) - else: - ensure_connection(session, goptions) - if selection: - tasks = [task['id'] for task in _list_tasks(options, session)] - if not tasks: - print("(no tasks)") - return - else: - tasks = [] - for task in args: - try: - tasks.append(int(task)) - except ValueError: - parser.error("task id must be an integer") - if not tasks: - parser.error("at least one task id must be specified") - - return watch_tasks(session, tasks, quiet=options.quiet, - poll_interval=goptions.poll_interval, topurl=goptions.topurl) - - diff --git a/cli/koji_cli/commands/cmd_win_build.py b/cli/koji_cli/commands/cmd_win_build.py deleted file mode 100644 index 661c164..0000000 --- a/cli/koji_cli/commands/cmd_win_build.py +++ /dev/null @@ -1,87 +0,0 @@ -from __future__ import absolute_import, division - -from optparse import OptionParser - - - -from koji_cli.lib import ( - _running_in_bg, - activate_session, - get_usage_str, - watch_tasks -) - - -def handle_win_build(options, session, args): - """[build] Build a Windows package from source""" - # Usage & option parsing - usage = "usage: %prog win-build [options] " - parser = OptionParser(usage=get_usage_str(usage)) - parser.add_option("--winspec", metavar="URL", - help="SCM URL to retrieve the build descriptor from. " - "If not specified, the winspec must be in the root directory " - "of the source repository.") - parser.add_option("--patches", metavar="URL", - help="SCM URL of a directory containing patches to apply " - "to the sources before building") - parser.add_option("--cpus", type="int", - help="Number of cpus to allocate to the build VM " - "(requires admin access)") - parser.add_option("--mem", type="int", - help="Amount of memory (in megabytes) to allocate to the build VM " - "(requires admin access)") - parser.add_option("--static-mac", action="store_true", - help="Retain the original MAC address when cloning the VM") - parser.add_option("--specfile", metavar="URL", - help="SCM URL of a spec file fragment to use to generate wrapper RPMs") - parser.add_option("--scratch", action="store_true", - help="Perform a scratch build") - parser.add_option("--repo-id", type="int", help="Use a specific repo") - parser.add_option("--skip-tag", action="store_true", help="Do not attempt to tag package") - parser.add_option("--background", action="store_true", - help="Run the build at a lower priority") - parser.add_option("--wait", action="store_true", - help="Wait on the build, even if running in the background") - parser.add_option("--nowait", action="store_false", dest="wait", help="Don't wait on build") - parser.add_option("--quiet", action="store_true", - help="Do not print the task information", default=options.quiet) - (build_opts, args) = parser.parse_args(args) - if len(args) != 3: - parser.error( - "Exactly three arguments (a build target, a SCM URL, and a VM name) are required") - activate_session(session, options) - target = args[0] - if target.lower() == "none" and build_opts.repo_id: - target = None - build_opts.skip_tag = True - else: - build_target = session.getBuildTarget(target) - if not build_target: - parser.error("No such build target: %s" % target) - dest_tag = session.getTag(build_target['dest_tag']) - if not dest_tag: - parser.error("No such destination tag: %s" % build_target['dest_tag_name']) - if dest_tag['locked'] and not build_opts.scratch: - parser.error("Destination tag %s is locked" % dest_tag['name']) - scmurl = args[1] - vm_name = args[2] - opts = {} - for key in ('winspec', 'patches', 'cpus', 'mem', 'static_mac', - 'specfile', 'scratch', 'repo_id', 'skip_tag'): - val = getattr(build_opts, key) - if val is not None: - opts[key] = val - priority = None - if build_opts.background: - # relative to koji.PRIO_DEFAULT - priority = 5 - task_id = session.winBuild(vm_name, scmurl, target, opts, priority=priority) - if not build_opts.quiet: - print("Created task: %d" % task_id) - print("Task info: %s/taskinfo?taskID=%s" % (options.weburl, task_id)) - if build_opts.wait or (build_opts.wait is None and not _running_in_bg()): - session.logout() - return watch_tasks(session, [task_id], quiet=build_opts.quiet, - poll_interval=options.poll_interval, topurl=options.topurl) - - diff --git a/cli/koji_cli/commands/cmd_wrapper_rpm.py b/cli/koji_cli/commands/cmd_wrapper_rpm.py deleted file mode 100644 index acdb8d6..0000000 --- a/cli/koji_cli/commands/cmd_wrapper_rpm.py +++ /dev/null @@ -1,87 +0,0 @@ -from __future__ import absolute_import, division - -from optparse import OptionParser - - -import koji -from koji.util import to_list - -from koji_cli.lib import ( - _running_in_bg, - activate_session, - get_usage_str, - watch_tasks -) - - -def handle_wrapper_rpm(options, session, args): - """[build] Build wrapper rpms for any archives associated with a build.""" - usage = "usage: %prog wrapper-rpm [options] " - parser = OptionParser(usage=get_usage_str(usage)) - parser.add_option("--create-build", action="store_true", - help="Create a new build to contain wrapper rpms") - parser.add_option("--ini", action="append", dest="inis", metavar="CONFIG", default=[], - help="Pass build parameters via a .ini file") - parser.add_option("-s", "--section", - help="Get build parameters from this section of the .ini") - parser.add_option("--skip-tag", action="store_true", - help="If creating a new build, don't tag it") - parser.add_option("--scratch", action="store_true", help="Perform a scratch build") - parser.add_option("--wait", action="store_true", - help="Wait on build, even if running in the background") - parser.add_option("--nowait", action="store_false", dest="wait", help="Don't wait on build") - parser.add_option("--background", action="store_true", - help="Run the build at a lower priority") - - (build_opts, args) = parser.parse_args(args) - if build_opts.inis: - if len(args) != 1: - parser.error("Exactly one argument (a build target) is required") - else: - if len(args) < 3: - parser.error("You must provide a build target, a build ID or NVR, " - "and a SCM URL to a specfile fragment") - activate_session(session, options) - - target = args[0] - if build_opts.inis: - try: - params = koji.util.parse_maven_param(build_opts.inis, scratch=build_opts.scratch, - section=build_opts.section) - except ValueError as e: - parser.error(e.args[0]) - opts = to_list(params.values())[0] - if opts.get('type') != 'wrapper': - parser.error("Section %s does not contain a wrapper-rpm config" % - to_list(params.keys())[0]) - url = opts['scmurl'] - package = opts['buildrequires'][0] - target_info = session.getBuildTarget(target, strict=True) - latest_builds = session.getLatestBuilds(target_info['dest_tag'], package=package) - if not latest_builds: - parser.error("No build of %s in %s" % (package, target_info['dest_tag_name'])) - build_id = latest_builds[0]['nvr'] - else: - build_id = args[1] - if build_id.isdigit(): - build_id = int(build_id) - url = args[2] - priority = None - if build_opts.background: - priority = 5 - opts = {} - if build_opts.create_build: - opts['create_build'] = True - if build_opts.skip_tag: - opts['skip_tag'] = True - if build_opts.scratch: - opts['scratch'] = True - task_id = session.wrapperRPM(build_id, url, target, priority, opts=opts) - print("Created task: %d" % task_id) - print("Task info: %s/taskinfo?taskID=%s" % (options.weburl, task_id)) - if build_opts.wait or (build_opts.wait is None and not _running_in_bg()): - session.logout() - return watch_tasks(session, [task_id], quiet=options.quiet, - poll_interval=options.poll_interval, topurl=options.topurl) - - diff --git a/cli/koji_cli/commands/cmd_write_signed_rpm.py b/cli/koji_cli/commands/cmd_write_signed_rpm.py deleted file mode 100644 index 3367586..0000000 --- a/cli/koji_cli/commands/cmd_write_signed_rpm.py +++ /dev/null @@ -1,69 +0,0 @@ -from __future__ import absolute_import, division - -from optparse import OptionParser - -from six.moves import zip - -import koji - -from koji_cli.lib import ( - activate_session, - get_usage_str -) - - -def handle_write_signed_rpm(goptions, session, args): - "[admin] Write signed RPMs to disk" - usage = "usage: %prog write-signed-rpm [options] [ ...]" - parser = OptionParser(usage=get_usage_str(usage)) - parser.add_option("--all", action="store_true", help="Write out all RPMs signed with this key") - parser.add_option("--buildid", help="Specify a build id rather than an n-v-r") - (options, args) = parser.parse_args(args) - if len(args) < 1: - parser.error("A signature key must be specified") - if len(args) < 2 and not (options.all or options.buildid): - parser.error("At least one RPM must be specified") - key = args.pop(0).lower() - activate_session(session, goptions) - if options.all: - rpms = session.queryRPMSigs(sigkey=key) - with session.multicall() as m: - results = [m.getRPM(r['rpm_id']) for r in rpms] - rpms = [x.result for x in results] - elif options.buildid: - rpms = session.listRPMs(int(options.buildid)) - else: - nvrs = [] - rpms = [] - - with session.multicall() as m: - result = [m.getRPM(nvra, strict=False) for nvra in args] - for rpm, nvra in zip(result, args): - rpm = rpm.result - if rpm: - rpms.append(rpm) - else: - nvrs.append(nvra) - - # for historical reasons, we also accept nvrs - with session.multicall() as m: - result = [m.getBuild(nvr, strict=True) for nvr in nvrs] - builds = [] - for nvr, build in zip(nvrs, result): - try: - builds.append(build.result['id']) - except koji.GenericError: - raise koji.GenericError("No such rpm or build: %s" % nvr) - - with session.multicall() as m: - rpm_lists = [m.listRPMs(buildID=build_id) for build_id in builds] - for rpm_list in rpm_lists: - rpms.extend(rpm_list.result) - - with session.multicall(strict=True) as m: - for i, rpminfo in enumerate(rpms): - nvra = "%(name)s-%(version)s-%(release)s.%(arch)s" % rpminfo - print("[%d/%d] %s" % (i + 1, len(rpms), nvra)) - m.writeSignedRPM(rpminfo['id'], key) - - diff --git a/cli/koji_cli/commands/disable_channel.py b/cli/koji_cli/commands/disable_channel.py new file mode 100644 index 0000000..33ce167 --- /dev/null +++ b/cli/koji_cli/commands/disable_channel.py @@ -0,0 +1,37 @@ +from __future__ import absolute_import, division + +from optparse import OptionParser + +from six.moves import zip + + +from koji_cli.lib import ( + activate_session, + error, + get_usage_str +) + + +def handle_disable_channel(goptions, session, args): + "[admin] Mark one or more channels as disabled" + usage = "usage: %prog disable-channel [options] [ ...]" + parser = OptionParser(usage=get_usage_str(usage)) + parser.add_option("--comment", help="Comment indicating why the channel(s) are being disabled") + (options, args) = parser.parse_args(args) + + if not args: + parser.error("At least one channel must be specified") + + activate_session(session, goptions) + + with session.multicall() as m: + result = [m.getChannel(channel, strict=False) for channel in args] + error_hit = False + for channel, id in zip(args, result): + if not id.result: + print("No such channel: %s" % channel) + error_hit = True + if error_hit: + error("No changes made. Please correct the command line.") + with session.multicall() as m: + [m.disableChannel(channel, comment=options.comment) for channel in args] diff --git a/cli/koji_cli/commands/disable_host.py b/cli/koji_cli/commands/disable_host.py new file mode 100644 index 0000000..03535b7 --- /dev/null +++ b/cli/koji_cli/commands/disable_host.py @@ -0,0 +1,41 @@ +from __future__ import absolute_import, division + +from optparse import OptionParser + +from six.moves import zip + + +from koji_cli.lib import ( + activate_session, + error, + get_usage_str +) + + +def handle_disable_host(goptions, session, args): + "[admin] Mark one or more hosts as disabled" + usage = "usage: %prog disable-host [options] [ ...]" + parser = OptionParser(usage=get_usage_str(usage)) + parser.add_option("--comment", help="Comment indicating why the host(s) are being disabled") + (options, args) = parser.parse_args(args) + + if not args: + parser.error("At least one host must be specified") + + activate_session(session, goptions) + session.multicall = True + for host in args: + session.getHost(host) + error_hit = False + for host, [id] in zip(args, session.multiCall(strict=True)): + if not id: + print("No such host: %s" % host) + error_hit = True + if error_hit: + error("No changes made. Please correct the command line.") + session.multicall = True + for host in args: + session.disableHost(host) + if options.comment is not None: + session.editHost(host, comment=options.comment) + session.multiCall(strict=True) diff --git a/cli/koji_cli/commands/disable_user.py b/cli/koji_cli/commands/disable_user.py new file mode 100644 index 0000000..e75026b --- /dev/null +++ b/cli/koji_cli/commands/disable_user.py @@ -0,0 +1,23 @@ +from __future__ import absolute_import, division + +from optparse import OptionParser + + +from koji_cli.lib import ( + activate_session, + get_usage_str +) + + +def handle_disable_user(goptions, session, args): + "[admin] Disable logins by a user" + usage = "usage: %prog disable-user " + parser = OptionParser(usage=get_usage_str(usage)) + (options, args) = parser.parse_args(args) + if len(args) < 1: + parser.error("You must specify the username of the user to disable") + elif len(args) > 1: + parser.error("This command only accepts one argument (username)") + username = args[0] + activate_session(session, goptions) + session.disableUser(username) diff --git a/cli/koji_cli/commands/dist_repo.py b/cli/koji_cli/commands/dist_repo.py new file mode 100644 index 0000000..9258e2f --- /dev/null +++ b/cli/koji_cli/commands/dist_repo.py @@ -0,0 +1,160 @@ +from __future__ import absolute_import, division + +import os +from optparse import OptionParser + +import koji + +from koji_cli.lib import ( + _progress_callback, + _running_in_bg, + activate_session, + unique_path, + warn, + watch_tasks +) + + +def handle_dist_repo(options, session, args): + """Create a yum repo with distribution options""" + usage = "usage: %prog dist-repo [options] [ ...]\n\n" \ + "In normal mode, dist-repo behaves like any other koji task.\n" \ + "Sometimes you want to limit running distRepo tasks per tag to only\n" \ + "one. For such behaviour admin (with 'tag' permission) needs to\n" \ + "modify given tag's extra field 'distrepo.cancel_others' to True'\n" \ + "via 'koji edit-tag -x distrepo.cancel_others=True'\n" + usage += "\n(Specify the --help option for a list of other options)" + parser = OptionParser(usage=usage) + parser.add_option('--allow-missing-signatures', action='store_true', + default=False, + help='For RPMs not signed with a desired key, fall back to the primary copy') + parser.add_option("-a", "--arch", action='append', default=[], + help="Indicate an architecture to consider. The default is all " + "architectures associated with the given tag. This option may " + "be specified multiple times.") + parser.add_option("--with-src", action='store_true', help='Also generate a src repo') + parser.add_option("--split-debuginfo", action='store_true', default=False, + help='Split debuginfo info a separate repo for each arch') + parser.add_option('--comps', help='Include a comps file in the repodata') + parser.add_option('--delta-rpms', metavar='REPO', default=[], action='append', + help='Create delta rpms. REPO can be the id of another dist repo ' + 'or the name of a tag that has a dist repo. May be specified ' + 'multiple times.') + parser.add_option('--event', type='int', help='Use tag content at event') + parser.add_option("--volume", help="Generate repo on given volume") + parser.add_option('--non-latest', dest='latest', default=True, + action='store_false', help='Include older builds, not just the latest') + parser.add_option('--multilib', default=None, metavar="CONFIG", + help='Include multilib packages in the repository using the given ' + 'config file') + parser.add_option("--noinherit", action='store_true', default=False, + help='Do not consider tag inheritance') + parser.add_option("--wait", action="store_true", + help="Wait for the task to complete, even if running in the background") + parser.add_option("--nowait", action="store_false", dest="wait", + help="Do not wait for the task to complete") + parser.add_option('--skip-missing-signatures', action='store_true', default=False, + help='Skip RPMs not signed with the desired key(s)') + parser.add_option('--zck', action='store_true', default=False, + help='Generate zchunk files as well as the standard repodata') + parser.add_option('--zck-dict-dir', action='store', default=None, + help='Directory containing compression dictionaries for use by zchunk ' + '(on builder)') + parser.add_option("--write-signed-rpms", action='store_true', default=False, + help='Write a signed rpms for given tag') + task_opts, args = parser.parse_args(args) + if len(args) < 1: + parser.error('You must provide a tag to generate the repo from') + if len(args) < 2 and not task_opts.allow_missing_signatures: + parser.error('Please specify one or more GPG key IDs (or --allow-missing-signatures)') + if task_opts.allow_missing_signatures and task_opts.skip_missing_signatures: + parser.error('allow_missing_signatures and skip_missing_signatures are mutually exclusive') + activate_session(session, options) + stuffdir = unique_path('cli-dist-repo') + if task_opts.comps: + if not os.path.exists(task_opts.comps): + parser.error('could not find %s' % task_opts.comps) + session.uploadWrapper(task_opts.comps, stuffdir, + callback=_progress_callback) + print('') + task_opts.comps = os.path.join(stuffdir, + os.path.basename(task_opts.comps)) + old_repos = [] + if len(task_opts.delta_rpms) > 0: + for repo in task_opts.delta_rpms: + if repo.isdigit(): + rinfo = session.repoInfo(int(repo), strict=True) + else: + # get dist repo for tag + rinfo = session.getRepo(repo, dist=True) + if not rinfo: + # maybe there is an expired one + rinfo = session.getRepo(repo, + state=koji.REPO_STATES['EXPIRED'], dist=True) + if not rinfo: + parser.error("Can't find repo for tag: %s" % repo) + old_repos.append(rinfo['id']) + tag = args[0] + keys = args[1:] + taginfo = session.getTag(tag) + if not taginfo: + parser.error('No such tag: %s' % tag) + allowed_arches = taginfo['arches'] or '' + if not allowed_arches: + for tag_inh in session.getFullInheritance(tag): + allowed_arches = session.getTag(tag_inh['parent_id'])['arches'] or '' + if allowed_arches: + break + if len(task_opts.arch) == 0: + task_opts.arch = allowed_arches.split() + if not task_opts.arch: + parser.error('No arches given and no arches associated with tag') + else: + for a in task_opts.arch: + if not allowed_arches: + warn('Tag %s has an empty arch list' % taginfo['name']) + elif a not in allowed_arches: + warn('%s is not in the list of tag arches' % a) + if task_opts.multilib: + if not os.path.exists(task_opts.multilib): + parser.error('could not find %s' % task_opts.multilib) + if 'x86_64' in task_opts.arch and 'i686' not in task_opts.arch: + parser.error('The multilib arch (i686) must be included') + if 's390x' in task_opts.arch and 's390' not in task_opts.arch: + parser.error('The multilib arch (s390) must be included') + if 'ppc64' in task_opts.arch and 'ppc' not in task_opts.arch: + parser.error('The multilib arch (ppc) must be included') + session.uploadWrapper(task_opts.multilib, stuffdir, + callback=_progress_callback) + task_opts.multilib = os.path.join(stuffdir, + os.path.basename(task_opts.multilib)) + print('') + if 'noarch' in task_opts.arch: + task_opts.arch.remove('noarch') + if task_opts.with_src and 'src' not in task_opts.arch: + task_opts.arch.append('src') + if not task_opts.arch: + parser.error('No arches left.') + + opts = { + 'arch': task_opts.arch, + 'comps': task_opts.comps, + 'delta': old_repos, + 'event': task_opts.event, + 'volume': task_opts.volume, + 'inherit': not task_opts.noinherit, + 'latest': task_opts.latest, + 'multilib': task_opts.multilib, + 'split_debuginfo': task_opts.split_debuginfo, + 'skip_missing_signatures': task_opts.skip_missing_signatures, + 'allow_missing_signatures': task_opts.allow_missing_signatures, + 'zck': task_opts.zck, + 'zck_dict_dir': task_opts.zck_dict_dir, + 'write_signed_rpms': task_opts.write_signed_rpms, + } + task_id = session.distRepo(tag, keys, **opts) + print("Creating dist repo for tag " + tag) + if task_opts.wait or (task_opts.wait is None and not _running_in_bg()): + session.logout() + return watch_tasks(session, [task_id], quiet=options.quiet, + poll_interval=options.poll_interval, topurl=options.topurl) diff --git a/cli/koji_cli/commands/download_build.py b/cli/koji_cli/commands/download_build.py new file mode 100644 index 0000000..93447e1 --- /dev/null +++ b/cli/koji_cli/commands/download_build.py @@ -0,0 +1,130 @@ +from __future__ import absolute_import, division + +from optparse import OptionParser + +from six.moves import zip + +import koji + +from koji_cli.lib import ( + download_archive, + download_rpm, + ensure_connection, + error, + get_usage_str, + warn +) + + +def anon_handle_download_build(options, session, args): + "[download] Download a built package" + usage = "usage: %prog download-build [options] " + parser = OptionParser(usage=get_usage_str(usage)) + parser.add_option("--arch", "-a", dest="arches", metavar="ARCH", action="append", default=[], + help="Only download packages for this arch (may be used multiple times)") + parser.add_option("--type", + help="Download archives of the given type, rather than rpms " + "(maven, win, image, remote-sources)") + parser.add_option("--latestfrom", dest="latestfrom", + help="Download the latest build from this tag") + parser.add_option("--debuginfo", action="store_true", help="Also download -debuginfo rpms") + parser.add_option("--task-id", action="store_true", help="Interperet id as a task id") + parser.add_option("--rpm", action="store_true", help="Download the given rpm") + parser.add_option("--key", help="Download rpms signed with the given key") + parser.add_option("--topurl", metavar="URL", default=options.topurl, + help="URL under which Koji files are accessible") + parser.add_option("--noprogress", action="store_true", help="Do not display progress meter") + parser.add_option("-q", "--quiet", action="store_true", + help="Suppress output", default=options.quiet) + (suboptions, args) = parser.parse_args(args) + if len(args) < 1: + parser.error("Please specify a package N-V-R or build ID") + elif len(args) > 1: + parser.error("Only a single package N-V-R or build ID may be specified") + + ensure_connection(session, options) + build = args[0] + + if build.isdigit(): + if suboptions.latestfrom: + parser.error("--latestfrom not compatible with build IDs, specify a package name.") + build = int(build) + if suboptions.task_id: + builds = session.listBuilds(taskID=build) + if not builds: + error("No associated builds for task %s" % build) + build = builds[0]['build_id'] + + if suboptions.latestfrom: + # We want the latest build, not a specific build + try: + builds = session.listTagged(suboptions.latestfrom, latest=True, package=build, + type=suboptions.type) + except koji.GenericError as data: + error("Error finding latest build: %s" % data) + if not builds: + error("%s has no builds of %s" % (suboptions.latestfrom, build)) + info = builds[0] + elif suboptions.rpm: + rpminfo = session.getRPM(build) + if rpminfo is None: + error("No such rpm: %s" % build) + info = session.getBuild(rpminfo['build_id']) + else: + # if we're given an rpm name without --rpm, download the containing build + try: + koji.parse_NVRA(build) + rpminfo = session.getRPM(build) + build = rpminfo['build_id'] + except Exception: + pass + info = session.getBuild(build) + + if info is None: + error("No such build: %s" % build) + + if not suboptions.topurl: + error("You must specify --topurl to download files") + + archives = [] + rpms = [] + if suboptions.type: + archives = session.listArchives(buildID=info['id'], type=suboptions.type) + if not archives: + error("No %s archives available for %s" % (suboptions.type, koji.buildLabel(info))) + else: + arches = suboptions.arches + if len(arches) == 0: + arches = None + if suboptions.rpm: + all_rpms = [rpminfo] + else: + all_rpms = session.listRPMs(buildID=info['id'], arches=arches) + if not all_rpms: + if arches: + error("No %s packages available for %s" % + (" or ".join(arches), koji.buildLabel(info))) + else: + error("No packages available for %s" % koji.buildLabel(info)) + for rpm in all_rpms: + if not suboptions.debuginfo and koji.is_debuginfo(rpm['name']): + continue + rpms.append(rpm) + + if suboptions.key: + with session.multicall() as m: + results = [m.queryRPMSigs(rpm_id=r['id'], sigkey=suboptions.key) for r in rpms] + rpm_keys = [x.result for x in results] + for rpm, rpm_key in list(zip(rpms, rpm_keys)): + if not rpm_key: + nvra = "%(nvr)s-%(arch)s.rpm" % rpm + warn("No such sigkey %s for rpm %s" % (suboptions.key, nvra)) + rpms.remove(rpm) + + # run the download + for rpm in rpms: + download_rpm(info, rpm, suboptions.topurl, sigkey=suboptions.key, + quiet=suboptions.quiet, noprogress=suboptions.noprogress) + for archive in archives: + download_archive(info, archive, suboptions.topurl, + quiet=suboptions.quiet, noprogress=suboptions.noprogress) diff --git a/cli/koji_cli/commands/download_logs.py b/cli/koji_cli/commands/download_logs.py new file mode 100644 index 0000000..c7be240 --- /dev/null +++ b/cli/koji_cli/commands/download_logs.py @@ -0,0 +1,166 @@ +from __future__ import absolute_import, division + +import os +import pprint +import sys +import traceback +from optparse import OptionParser + +import six +import six.moves.xmlrpc_client + +import koji + +from koji_cli.lib import ( + download_file, + ensure_connection, + error, + get_usage_str, + list_task_output_all_volumes, + warn +) + + +def anon_handle_download_logs(options, session, args): + "[download] Download logs for task" + + FAIL_LOG = "task_failed.log" + usage = "usage: %prog download-logs [options] [ ...]" + usage += "\n %prog download-logs [options] --nvr [ ...]" + usage += "\n" + usage += "\n" + usage += "Note this command only downloads task logs, not build logs." + usage += "\n" + parser = OptionParser(usage=get_usage_str(usage)) + parser.add_option("-r", "--recurse", action="store_true", + help="Process children of this task as well") + parser.add_option("--nvr", action="store_true", + help="Get the logs for the task associated with this build " + "Name-Version-Release.") + parser.add_option("-m", "--match", action="append", metavar="PATTERN", + help="Get only log filenames matching PATTERN (fnmatch). " + "May be used multiple times.") + parser.add_option("-c", "--continue", action="store_true", dest="cont", + help="Continue previous download") + parser.add_option("-d", "--dir", metavar="DIRECTORY", default='kojilogs', + help="Write logs to DIRECTORY") + (suboptions, args) = parser.parse_args(args) + + if len(args) < 1: + parser.error("Please specify at least one task id or n-v-r") + + def write_fail_log(task_log_dir, task_id): + """Gets output only from failed tasks""" + try: + result = session.getTaskResult(task_id) + # with current code, failed task results should always be faults, + # but that could change in the future + content = pprint.pformat(result) + except (six.moves.xmlrpc_client.Fault, koji.GenericError): + etype, e = sys.exc_info()[:2] + content = ''.join(traceback.format_exception_only(etype, e)) + full_filename = os.path.normpath(os.path.join(task_log_dir, FAIL_LOG)) + koji.ensuredir(os.path.dirname(full_filename)) + sys.stdout.write("Writing: %s\n" % full_filename) + with open(full_filename, 'wt') as fo: + fo.write(content) + + def download_log(task_log_dir, task_id, filename, blocksize=102400, volume=None): + # Create directories only if there is any log file to write to + # For each non-default volume create special sub-directory + if volume not in (None, 'DEFAULT'): + full_filename = os.path.normpath(os.path.join(task_log_dir, volume, filename)) + else: + full_filename = os.path.normpath(os.path.join(task_log_dir, filename)) + koji.ensuredir(os.path.dirname(full_filename)) + contents = 'IGNORE ME!' + if suboptions.cont and os.path.exists(full_filename): + sys.stdout.write("Continuing: %s\n" % full_filename) + fd = open(full_filename, 'ab') + offset = fd.tell() + else: + sys.stdout.write("Downloading: %s\n" % full_filename) + fd = open(full_filename, 'wb') + offset = 0 + try: + while contents: + contents = session.downloadTaskOutput(task_id, filename, offset=offset, + size=blocksize, volume=volume) + offset += len(contents) + if contents: + fd.write(contents) + finally: + fd.close() + + def save_logs(task_id, match, parent_dir='.', recurse=True): + assert task_id == int(task_id), "Task id must be number: %r" % task_id + task_info = session.getTaskInfo(task_id) + if task_info is None: + error("No such task: %d" % task_id) + files = list_task_output_all_volumes(session, task_id) + logs = [] # list of tuples (filename, volume) + for filename in files: + if not filename.endswith(".log"): + continue + if match and not koji.util.multi_fnmatch(filename, match): + continue + logs += [(filename, volume) for volume in files[filename]] + + task_log_dir = os.path.join(parent_dir, + "%s-%s" % (task_info["arch"], task_id)) + + count = 0 + state = koji.TASK_STATES[task_info['state']] + if state == 'FAILED': + if not match or koji.util.multi_fnmatch(FAIL_LOG, match): + write_fail_log(task_log_dir, task_id) + count += 1 + elif state not in ['CLOSED', 'CANCELED']: + warn("Task %s is %s\n" % (task_id, state)) + + for log_filename, log_volume in logs: + download_log(task_log_dir, task_id, log_filename, volume=log_volume) + count += 1 + + if count == 0 and not recurse: + warn("No logs found for task %i. Perhaps try --recurse?\n" % task_id) + + if recurse: + child_tasks = session.getTaskChildren(task_id) + for child_task in child_tasks: + save_logs(child_task['id'], match, task_log_dir, recurse) + + ensure_connection(session, options) + task_id = None + build_id = None + for arg in args: + if suboptions.nvr: + suboptions.recurse = True + binfo = session.getBuild(arg) + if binfo is None: + error("There is no build with n-v-r: %s" % arg) + if binfo.get('task_id'): + task_id = binfo['task_id'] + sys.stdout.write("Using task ID: %s\n" % task_id) + elif binfo.get('build_id'): + build_id = binfo['build_id'] + sys.stdout.write("Using build ID: %s\n" % build_id) + else: + try: + task_id = int(arg) + except ValueError: + error("Task id must be number: %r" % arg) + if task_id: + save_logs(task_id, suboptions.match, suboptions.dir, suboptions.recurse) + elif build_id: + logs = session.getBuildLogs(build_id) + match = suboptions.match + for log in logs: + url = os.path.join(options.topurl, log['path']) + filepath = os.path.join(os.getcwd(), '%s/%s/%s' % (suboptions.dir, + arg, log['name'])) + if not filepath.endswith(".log"): + continue + if match and not koji.util.multi_fnmatch(log['name'], match): + continue + download_file(url, filepath) diff --git a/cli/koji_cli/commands/download_task.py b/cli/koji_cli/commands/download_task.py new file mode 100644 index 0000000..a7331a5 --- /dev/null +++ b/cli/koji_cli/commands/download_task.py @@ -0,0 +1,120 @@ +from __future__ import absolute_import, division + +import os +from optparse import OptionParser + +from six.moves import filter + +import koji + +from koji_cli.lib import ( + _running_in_bg, + download_file, + ensure_connection, + error, + get_usage_str, + list_task_output_all_volumes, + watch_tasks +) + + +def anon_handle_download_task(options, session, args): + "[download] Download the output of a build task" + usage = "usage: %prog download-task " + parser = OptionParser(usage=get_usage_str(usage)) + parser.add_option("--arch", dest="arches", metavar="ARCH", action="append", default=[], + help="Only download packages for this arch (may be used multiple times)") + parser.add_option("--logs", dest="logs", action="store_true", default=False, + help="Also download build logs") + parser.add_option("--topurl", metavar="URL", default=options.topurl, + help="URL under which Koji files are accessible") + parser.add_option("--noprogress", action="store_true", help="Do not display progress meter") + parser.add_option("--wait", action="store_true", + help="Wait for running tasks to finish, even if running in the background") + parser.add_option("--nowait", action="store_false", dest="wait", + help="Do not wait for running tasks to finish") + parser.add_option("-q", "--quiet", action="store_true", + help="Suppress output", default=options.quiet) + + (suboptions, args) = parser.parse_args(args) + if len(args) == 0: + parser.error("Please specify a task ID") + elif len(args) > 1: + parser.error("Only one task ID may be specified") + + base_task_id = int(args.pop()) + if len(suboptions.arches) > 0: + suboptions.arches = ",".join(suboptions.arches).split(",") + + ensure_connection(session, options) + + # get downloadable tasks + + base_task = session.getTaskInfo(base_task_id) + if not base_task: + error('No such task: %d' % base_task_id) + + if (suboptions.wait or (suboptions.wait is None and not _running_in_bg())) and \ + base_task['state'] not in ( + koji.TASK_STATES['CLOSED'], + koji.TASK_STATES['CANCELED'], + koji.TASK_STATES['FAILED']): + watch_tasks(session, [base_task_id], quiet=suboptions.quiet, + poll_interval=options.poll_interval, topurl=options.topurl) + + def check_downloadable(task): + return task["method"] == "buildArch" + + downloadable_tasks = [] + + if check_downloadable(base_task): + downloadable_tasks.append(base_task) + else: + subtasks = session.getTaskChildren(base_task_id) + downloadable_tasks.extend(list(filter(check_downloadable, subtasks))) + + # get files for download + downloads = [] + + for task in downloadable_tasks: + files = list_task_output_all_volumes(session, task["id"]) + for filename in files: + if filename.endswith(".rpm"): + for volume in files[filename]: + filearch = filename.split(".")[-2] + if len(suboptions.arches) == 0 or filearch in suboptions.arches: + downloads.append((task, filename, volume, filename)) + elif filename.endswith(".log") and suboptions.logs: + for volume in files[filename]: + # rename logs, they would conflict + new_filename = "%s.%s.log" % (filename.rstrip(".log"), task["arch"]) + downloads.append((task, filename, volume, new_filename)) + + if len(downloads) == 0: + error("No files for download found.") + + required_tasks = {} + for (task, nop, nop, nop) in downloads: + if task["id"] not in required_tasks: + required_tasks[task["id"]] = task + + for task_id in required_tasks: + if required_tasks[task_id]["state"] != koji.TASK_STATES.get("CLOSED"): + if task_id == base_task_id: + error("Task %d has not finished yet." % task_id) + else: + error("Child task %d has not finished yet." % task_id) + + # perform the download + number = 0 + pathinfo = koji.PathInfo(topdir=suboptions.topurl) + for (task, filename, volume, new_filename) in downloads: + number += 1 + if volume not in (None, 'DEFAULT'): + koji.ensuredir(volume) + new_filename = os.path.join(volume, new_filename) + if '..' in filename: + error('Invalid file name: %s' % filename) + url = '%s/%s/%s' % (pathinfo.work(volume), pathinfo.taskrelpath(task["id"]), filename) + download_file(url, new_filename, quiet=suboptions.quiet, noprogress=suboptions.noprogress, + size=len(downloads), num=number) diff --git a/cli/koji_cli/commands/edit_channel.py b/cli/koji_cli/commands/edit_channel.py new file mode 100644 index 0000000..f819728 --- /dev/null +++ b/cli/koji_cli/commands/edit_channel.py @@ -0,0 +1,43 @@ +from __future__ import absolute_import, division + +from optparse import OptionParser + +import koji + +from koji_cli.lib import ( + activate_session, + error, + get_usage_str +) + + +def handle_edit_channel(goptions, session, args): + "[admin] Edit a channel" + usage = "usage: %prog edit-channel [options] " + parser = OptionParser(usage=get_usage_str(usage)) + parser.add_option("--name", help="New channel name") + parser.add_option("--description", help="Description of channel") + parser.add_option("--comment", help="Comment of channel") + (options, args) = parser.parse_args(args) + if len(args) != 1: + parser.error("Incorrect number of arguments") + activate_session(session, goptions) + vals = {} + for key, val in options.__dict__.items(): + if val is not None: + vals[key] = val + cinfo = session.getChannel(args[0]) + if not cinfo: + error("No such channel: %s" % args[0]) + try: + result = session.editChannel(args[0], **vals) + except koji.GenericError as ex: + msg = str(ex) + if 'Invalid method:' in msg: + version = session.getKojiVersion() + error("editChannel is available on hub from Koji 1.26 version, your version is %s" % + version) + else: + print(msg) + if not result: + error("No changes made, please correct the command line") diff --git a/cli/koji_cli/commands/edit_external_repo.py b/cli/koji_cli/commands/edit_external_repo.py new file mode 100644 index 0000000..425b459 --- /dev/null +++ b/cli/koji_cli/commands/edit_external_repo.py @@ -0,0 +1,59 @@ +from __future__ import absolute_import, division + +from optparse import OptionParser + +import koji + +from koji_cli.lib import ( + activate_session, + get_usage_str +) + + +def handle_edit_external_repo(goptions, session, args): + "[admin] Edit data for an external repo" + usage = "usage: %prog edit-external-repo [options] " + parser = OptionParser(usage=get_usage_str(usage)) + parser.add_option("--url", help="Change the url") + parser.add_option("--name", help="Change the name") + parser.add_option("-t", "--tag", metavar="TAG", help="Edit the repo properties for the tag.") + parser.add_option("-p", "--priority", metavar="PRIORITY", type='int', + help="Edit the priority of the repo for the tag specified by --tag.") + parser.add_option("-m", "--mode", metavar="MODE", + help="Edit the merge mode of the repo for the tag specified by --tag. " + "Options: %s." % ", ".join(koji.REPO_MERGE_MODES)) + parser.add_option("-a", "--arches", metavar="ARCH1,ARCH2, ...", + help="Use only subset of arches from given repo") + (options, args) = parser.parse_args(args) + if len(args) != 1: + parser.error("Incorrect number of arguments") + repo_opts = {} + if options.url: + repo_opts['url'] = options.url + if options.name: + repo_opts['name'] = options.name + tag_repo_opts = {} + if options.tag: + if options.priority is not None: + tag_repo_opts['priority'] = options.priority + if options.mode: + tag_repo_opts['merge_mode'] = options.mode + if options.arches is not None: + tag_repo_opts['arches'] = options.arches + if not tag_repo_opts: + parser.error("At least, one of priority and merge mode should be specified") + tag_repo_opts['tag_info'] = options.tag + tag_repo_opts['repo_info'] = args[0] + else: + for k in ('priority', 'mode', 'arches'): + if getattr(options, k) is not None: + parser.error("If %s is specified, --tag must be specified as well" % k) + + if not (repo_opts or tag_repo_opts): + parser.error("No changes specified") + + activate_session(session, goptions) + if repo_opts: + session.editExternalRepo(args[0], **repo_opts) + if tag_repo_opts: + session.editTagExternalRepo(**tag_repo_opts) diff --git a/cli/koji_cli/commands/edit_host.py b/cli/koji_cli/commands/edit_host.py new file mode 100644 index 0000000..64efb09 --- /dev/null +++ b/cli/koji_cli/commands/edit_host.py @@ -0,0 +1,58 @@ +from __future__ import absolute_import, division + +from optparse import OptionParser + +from six.moves import zip + +import koji + +from koji_cli.lib import ( + activate_session, + error, + get_usage_str, + warn +) + + +def handle_edit_host(options, session, args): + "[admin] Edit a host" + usage = "usage: %prog edit-host [ ...] [options]" + parser = OptionParser(usage=get_usage_str(usage)) + parser.add_option("--arches", + help="Space or comma-separated list of supported architectures") + parser.add_option("--capacity", type="float", help="Capacity of this host") + parser.add_option("--description", metavar="DESC", help="Description of this host") + parser.add_option("--comment", help="A brief comment about this host") + (subopts, args) = parser.parse_args(args) + if not args: + parser.error("Please specify a hostname") + + activate_session(session, options) + + vals = {} + for key, val in subopts.__dict__.items(): + if val is not None: + vals[key] = val + if 'arches' in vals: + vals['arches'] = koji.parse_arches(vals['arches']) + + session.multicall = True + for host in args: + session.getHost(host) + error_hit = False + for host, [info] in zip(args, session.multiCall(strict=True)): + if not info: + warn("No such host: %s" % host) + error_hit = True + + if error_hit: + error("No changes made, please correct the command line") + + session.multicall = True + for host in args: + session.editHost(host, **vals) + for host, [result] in zip(args, session.multiCall(strict=True)): + if result: + print("Edited %s" % host) + else: + print("No changes made to %s" % host) diff --git a/cli/koji_cli/commands/edit_notification.py b/cli/koji_cli/commands/edit_notification.py new file mode 100644 index 0000000..dcdf86e --- /dev/null +++ b/cli/koji_cli/commands/edit_notification.py @@ -0,0 +1,64 @@ +from __future__ import absolute_import, division + +from optparse import OptionParser + +import koji + +from koji_cli.lib import ( + activate_session, + get_usage_str +) + + +def handle_edit_notification(goptions, session, args): + "[monitor] Edit user's notification" + usage = "usage: %prog edit-notification [options] " + parser = OptionParser(usage=get_usage_str(usage)) + parser.add_option("--package", help="Notifications for this package, '*' for all") + parser.add_option("--tag", help="Notifications for this tag, '*' for all") + parser.add_option("--success-only", action="store_true", default=None, + dest='success_only', help="Notify only on successful events") + parser.add_option("--no-success-only", action="store_false", + default=None, dest='success_only', help="Notify on all events") + (options, args) = parser.parse_args(args) + + if len(args) != 1: + parser.error("Only argument is notification ID") + + try: + n_id = int(args[0]) + except ValueError: + parser.error("Notification ID has to be numeric") + + if not options.package and not options.tag and options.success_only is None: + parser.error("Command need at least one option") + + activate_session(session, goptions) + + old = session.getBuildNotification(n_id) + + if options.package == '*': + package_id = None + elif options.package: + package_id = session.getPackageID(options.package) + if package_id is None: + parser.error("No such package: %s" % options.package) + else: + package_id = old['package_id'] + + if options.tag == '*': + tag_id = None + elif options.tag: + try: + tag_id = session.getTagID(options.tag, strict=True) + except koji.GenericError: + parser.error("No such tag: %s" % options.tag) + else: + tag_id = old['tag_id'] + + if options.success_only is not None: + success_only = options.success_only + else: + success_only = old['success_only'] + + session.updateNotification(n_id, package_id, tag_id, success_only) diff --git a/cli/koji_cli/commands/edit_permission.py b/cli/koji_cli/commands/edit_permission.py new file mode 100644 index 0000000..30e5eed --- /dev/null +++ b/cli/koji_cli/commands/edit_permission.py @@ -0,0 +1,22 @@ +from __future__ import absolute_import, division + +from optparse import OptionParser + + +from koji_cli.lib import ( + activate_session, + get_usage_str +) + + +def handle_edit_permission(goptions, session, args): + "[admin] Edit a permission description" + usage = "usage: %prog edit-permission " + parser = OptionParser(usage=get_usage_str(usage)) + (options, args) = parser.parse_args(args) + if len(args) < 2: + parser.error("Please specify a permission and a description") + activate_session(session, goptions) + perm = args[0] + description = args[1] + session.editPermission(perm, description) diff --git a/cli/koji_cli/commands/edit_tag.py b/cli/koji_cli/commands/edit_tag.py new file mode 100644 index 0000000..e488cf8 --- /dev/null +++ b/cli/koji_cli/commands/edit_tag.py @@ -0,0 +1,75 @@ +from __future__ import absolute_import, division + +from optparse import OptionParser + +import koji + +from koji_cli.lib import ( + activate_session, + arg_filter, + get_usage_str +) + + +def handle_edit_tag(goptions, session, args): + "[admin] Alter tag information" + usage = "usage: %prog edit-tag [options] " + parser = OptionParser(usage=get_usage_str(usage)) + parser.add_option("--arches", help="Specify arches") + parser.add_option("--perm", help="Specify permission requirement") + parser.add_option("--no-perm", action="store_true", help="Remove permission requirement") + parser.add_option("--lock", action="store_true", help="Lock the tag") + parser.add_option("--unlock", action="store_true", help="Unlock the tag") + parser.add_option("--rename", help="Rename the tag") + parser.add_option("--maven-support", action="store_true", + help="Enable creation of Maven repos for this tag") + parser.add_option("--no-maven-support", action="store_true", + help="Disable creation of Maven repos for this tag") + parser.add_option("--include-all", action="store_true", + help="Include all packages in this tag when generating Maven repos") + parser.add_option("--no-include-all", action="store_true", + help="Do not include all packages in this tag when generating Maven repos") + parser.add_option("-x", "--extra", action="append", default=[], metavar="key=value", + help="Set tag extra option. JSON-encoded or simple value") + parser.add_option("-r", "--remove-extra", action="append", default=[], metavar="key", + help="Remove tag extra option") + parser.add_option("-b", "--block-extra", action="append", default=[], metavar="key", + help="Block inherited tag extra option") + (options, args) = parser.parse_args(args) + if len(args) != 1: + parser.error("Please specify a name for the tag") + activate_session(session, goptions) + tag = args[0] + opts = {} + if options.arches: + opts['arches'] = koji.parse_arches(options.arches) + if options.no_perm: + opts['perm'] = None + elif options.perm: + opts['perm'] = options.perm + if options.unlock: + opts['locked'] = False + if options.lock: + opts['locked'] = True + if options.rename: + opts['name'] = options.rename + if options.maven_support: + opts['maven_support'] = True + if options.no_maven_support: + opts['maven_support'] = False + if options.include_all: + opts['maven_include_all'] = True + if options.no_include_all: + opts['maven_include_all'] = False + if options.extra: + extra = {} + for xopt in options.extra: + key, value = xopt.split('=', 1) + if key in extra: + parser.error("Duplicate extra key: %s" % key) + extra[key] = arg_filter(value, parse_json=True) + opts['extra'] = extra + opts['remove_extra'] = options.remove_extra + opts['block_extra'] = options.block_extra + # XXX change callname + session.editTag2(tag, **opts) diff --git a/cli/koji_cli/commands/edit_tag_inheritance.py b/cli/koji_cli/commands/edit_tag_inheritance.py new file mode 100644 index 0000000..5c492a4 --- /dev/null +++ b/cli/koji_cli/commands/edit_tag_inheritance.py @@ -0,0 +1,91 @@ +from __future__ import absolute_import, division + +from optparse import OptionParser + + +from koji_cli.lib import ( + activate_session, + error, + get_usage_str +) + + +def handle_edit_tag_inheritance(goptions, session, args): + """[admin] Edit tag inheritance""" + usage = "usage: %prog edit-tag-inheritance [options] " + parser = OptionParser(usage=get_usage_str(usage)) + parser.add_option("--priority", help="Specify a new priority") + parser.add_option("--maxdepth", help="Specify max depth") + parser.add_option("--intransitive", action="store_true", help="Set intransitive") + parser.add_option("--noconfig", action="store_true", help="Set to packages only") + parser.add_option("--pkg-filter", help="Specify the package filter") + (options, args) = parser.parse_args(args) + + if len(args) < 1: + parser.error("This command takes at least one argument: a tag name or ID") + + if len(args) > 3: + parser.error("This command takes at most three argument: a tag name or ID, " + "a parent tag name or ID, and a priority") + + activate_session(session, goptions) + + tag = session.getTag(args[0]) + if not tag: + parser.error("No such tag: %s" % args[0]) + + parent = None + priority = None + if len(args) > 1: + parent = session.getTag(args[1]) + if not parent: + parser.error("No such tag: %s" % args[1]) + if len(args) > 2: + priority = args[2] + + data = session.getInheritanceData(tag['id']) + if parent and data: + data = [datum for datum in data if datum['parent_id'] == parent['id']] + if priority and data: + data = [datum for datum in data if datum['priority'] == priority] + + if len(data) == 0: + error("No inheritance link found to remove. Please check your arguments") + elif len(data) > 1: + print("Multiple matches for tag.") + if not parent: + error("Please specify a parent on the command line.") + if not priority: + error("Please specify a priority on the command line.") + error("Error: Key constraints may be broken. Exiting.") + + # len(data) == 1 + data = data[0] + + inheritanceData = session.getInheritanceData(tag['id']) + samePriority = [datum for datum in inheritanceData if datum['priority'] == options.priority] + if samePriority: + error("Error: There is already an active inheritance with that priority on %s, " + "please specify a different priority with --priority." % tag['name']) + + new_data = data.copy() + if options.priority is not None and options.priority.isdigit(): + new_data['priority'] = int(options.priority) + if options.maxdepth is not None: + if options.maxdepth.isdigit(): + new_data['maxdepth'] = int(options.maxdepth) + elif options.maxdepth.lower() == "none": + new_data['maxdepth'] = None + else: + error("Invalid maxdepth: %s" % options.maxdepth) + if options.intransitive: + new_data['intransitive'] = options.intransitive + if options.noconfig: + new_data['noconfig'] = options.noconfig + if options.pkg_filter: + new_data['pkg_filter'] = options.pkg_filter + + # find the data we want to edit and replace it + index = inheritanceData.index(data) + inheritanceData[index] = new_data + session.setInheritanceData(tag['id'], inheritanceData) diff --git a/cli/koji_cli/commands/edit_target.py b/cli/koji_cli/commands/edit_target.py new file mode 100644 index 0000000..205b694 --- /dev/null +++ b/cli/koji_cli/commands/edit_target.py @@ -0,0 +1,53 @@ +from __future__ import absolute_import, division + +from optparse import OptionParser + +import koji + +from koji_cli.lib import ( + activate_session, + error, + get_usage_str +) + + +def handle_edit_target(goptions, session, args): + "[admin] Set the name, build_tag, and/or dest_tag of an existing build target to new values" + usage = "usage: %prog edit-target [options] " + parser = OptionParser(usage=get_usage_str(usage)) + parser.add_option("--rename", help="Specify new name for target") + parser.add_option("--build-tag", help="Specify a different build tag") + parser.add_option("--dest-tag", help="Specify a different destination tag") + + (options, args) = parser.parse_args(args) + + if len(args) != 1: + parser.error("Please specify a build target") + activate_session(session, goptions) + + if not (session.hasPerm('admin') or session.hasPerm('target')): + parser.error("This action requires target or admin privileges") + + targetInfo = session.getBuildTarget(args[0]) + if targetInfo is None: + raise koji.GenericError("No such build target: %s" % args[0]) + + targetInfo['orig_name'] = targetInfo['name'] + + if options.rename: + targetInfo['name'] = options.rename + if options.build_tag: + targetInfo['build_tag_name'] = options.build_tag + chkbuildtag = session.getTag(options.build_tag) + if not chkbuildtag: + error("No such tag: %s" % options.build_tag) + if not chkbuildtag.get("arches", None): + error("Build tag has no arches: %s" % options.build_tag) + if options.dest_tag: + chkdesttag = session.getTag(options.dest_tag) + if not chkdesttag: + error("No such destination tag: %s" % options.dest_tag) + targetInfo['dest_tag_name'] = options.dest_tag + + session.editBuildTarget(targetInfo['orig_name'], targetInfo['name'], + targetInfo['build_tag_name'], targetInfo['dest_tag_name']) diff --git a/cli/koji_cli/commands/edit_user.py b/cli/koji_cli/commands/edit_user.py new file mode 100644 index 0000000..73e9909 --- /dev/null +++ b/cli/koji_cli/commands/edit_user.py @@ -0,0 +1,39 @@ +from __future__ import absolute_import, division + +from optparse import OptionParser + + +from koji_cli.lib import ( + activate_session, + arg_filter, + get_usage_str +) + + +def handle_edit_user(goptions, session, args): + "[admin] Alter user information" + usage = "usage: %prog edit-user [options]" + parser = OptionParser(usage=get_usage_str(usage)) + parser.add_option("--rename", help="Rename the user") + parser.add_option("--edit-krb", action="append", default=[], metavar="OLD=NEW", + help="Change kerberos principal of the user") + parser.add_option("--add-krb", action="append", default=[], metavar="KRB", + help="Add kerberos principal of the user") + parser.add_option("--remove-krb", action="append", default=[], metavar="KRB", + help="Remove kerberos principal of the user") + (options, args) = parser.parse_args(args) + if len(args) < 1: + parser.error("You must specify the username of the user to edit") + elif len(args) > 1: + parser.error("This command only accepts one argument (username)") + activate_session(session, goptions) + user = args[0] + princ_mappings = [] + for p in options.edit_krb: + old, new = p.split('=', 1) + princ_mappings.append({'old': arg_filter(old), 'new': arg_filter(new)}) + for a in options.add_krb: + princ_mappings.append({'old': None, 'new': arg_filter(a)}) + for r in options.remove_krb: + princ_mappings.append({'old': arg_filter(r), 'new': None}) + session.editUser(user, options.rename, princ_mappings) diff --git a/cli/koji_cli/commands/enable_channel.py b/cli/koji_cli/commands/enable_channel.py new file mode 100644 index 0000000..1d5135f --- /dev/null +++ b/cli/koji_cli/commands/enable_channel.py @@ -0,0 +1,37 @@ +from __future__ import absolute_import, division + +from optparse import OptionParser + +from six.moves import zip + + +from koji_cli.lib import ( + activate_session, + error, + get_usage_str +) + + +def handle_enable_channel(goptions, session, args): + "[admin] Mark one or more channels as enabled" + usage = "usage: %prog enable-channel [options] [ ...]" + parser = OptionParser(usage=get_usage_str(usage)) + parser.add_option("--comment", help="Comment indicating why the channel(s) are being enabled") + (options, args) = parser.parse_args(args) + + if not args: + parser.error("At least one channel must be specified") + + activate_session(session, goptions) + with session.multicall() as m: + result = [m.getChannel(channel, strict=False) for channel in args] + error_hit = False + for channel, id in zip(args, result): + if not id.result: + print("No such channel: %s" % channel) + error_hit = True + if error_hit: + error("No changes made. Please correct the command line.") + + with session.multicall() as m: + [m.enableChannel(channel, comment=options.comment) for channel in args] diff --git a/cli/koji_cli/commands/enable_host.py b/cli/koji_cli/commands/enable_host.py new file mode 100644 index 0000000..f077f9c --- /dev/null +++ b/cli/koji_cli/commands/enable_host.py @@ -0,0 +1,41 @@ +from __future__ import absolute_import, division + +from optparse import OptionParser + +from six.moves import zip + + +from koji_cli.lib import ( + activate_session, + error, + get_usage_str +) + + +def handle_enable_host(goptions, session, args): + "[admin] Mark one or more hosts as enabled" + usage = "usage: %prog enable-host [options] [ ...]" + parser = OptionParser(usage=get_usage_str(usage)) + parser.add_option("--comment", help="Comment indicating why the host(s) are being enabled") + (options, args) = parser.parse_args(args) + + if not args: + parser.error("At least one host must be specified") + + activate_session(session, goptions) + session.multicall = True + for host in args: + session.getHost(host) + error_hit = False + for host, [id] in zip(args, session.multiCall(strict=True)): + if not id: + print("No such host: %s" % host) + error_hit = True + if error_hit: + error("No changes made. Please correct the command line.") + session.multicall = True + for host in args: + session.enableHost(host) + if options.comment is not None: + session.editHost(host, comment=options.comment) + session.multiCall(strict=True) diff --git a/cli/koji_cli/commands/enable_user.py b/cli/koji_cli/commands/enable_user.py new file mode 100644 index 0000000..ed339f7 --- /dev/null +++ b/cli/koji_cli/commands/enable_user.py @@ -0,0 +1,23 @@ +from __future__ import absolute_import, division + +from optparse import OptionParser + + +from koji_cli.lib import ( + activate_session, + get_usage_str +) + + +def handle_enable_user(goptions, session, args): + "[admin] Enable logins by a user" + usage = "usage: %prog enable-user " + parser = OptionParser(usage=get_usage_str(usage)) + (options, args) = parser.parse_args(args) + if len(args) < 1: + parser.error("You must specify the username of the user to enable") + elif len(args) > 1: + parser.error("This command only accepts one argument (username)") + username = args[0] + activate_session(session, goptions) + session.enableUser(username) diff --git a/cli/koji_cli/commands/free_task.py b/cli/koji_cli/commands/free_task.py new file mode 100644 index 0000000..fcef5b1 --- /dev/null +++ b/cli/koji_cli/commands/free_task.py @@ -0,0 +1,27 @@ +from __future__ import absolute_import, division + +from optparse import OptionParser + + +from koji_cli.lib import ( + activate_session, + get_usage_str +) + + +def handle_free_task(goptions, session, args): + "[admin] Free a task" + usage = "usage: %prog free-task [options] [ ...]" + parser = OptionParser(usage=get_usage_str(usage)) + (options, args) = parser.parse_args(args) + activate_session(session, goptions) + tlist = [] + for task_id in args: + try: + tlist.append(int(task_id)) + except ValueError: + parser.error("task_id must be an integer") + if not tlist: + parser.error("please specify at least one task_id") + for task_id in tlist: + session.freeTask(task_id) diff --git a/cli/koji_cli/commands/grant_cg_access.py b/cli/koji_cli/commands/grant_cg_access.py new file mode 100644 index 0000000..8f7ac1e --- /dev/null +++ b/cli/koji_cli/commands/grant_cg_access.py @@ -0,0 +1,29 @@ +from __future__ import absolute_import, division + +from optparse import OptionParser + + +from koji_cli.lib import ( + activate_session, + get_usage_str +) + + +def handle_grant_cg_access(goptions, session, args): + "[admin] Add a user to a content generator" + usage = "usage: %prog grant-cg-access " + parser = OptionParser(usage=get_usage_str(usage)) + parser.add_option("--new", action="store_true", help="Create a new content generator") + (options, args) = parser.parse_args(args) + if len(args) != 2: + parser.error("Please specify a user and content generator") + activate_session(session, goptions) + user = args[0] + cg = args[1] + uinfo = session.getUser(user) + if uinfo is None: + parser.error("No such user: %s" % user) + kwargs = {} + if options.new: + kwargs['create'] = True + session.grantCGAccess(uinfo['name'], cg, **kwargs) diff --git a/cli/koji_cli/commands/grant_permission.py b/cli/koji_cli/commands/grant_permission.py new file mode 100644 index 0000000..a431baa --- /dev/null +++ b/cli/koji_cli/commands/grant_permission.py @@ -0,0 +1,40 @@ +from __future__ import absolute_import, division + +from optparse import OptionParser + + +from koji_cli.lib import ( + activate_session, + get_usage_str +) + + +def handle_grant_permission(goptions, session, args): + "[admin] Grant a permission to a user" + usage = "usage: %prog grant-permission [options] [ ...]" + parser = OptionParser(usage=get_usage_str(usage)) + parser.add_option("--new", action="store_true", + help="Create this permission if the permission does not exist") + parser.add_option("--description", + help="Add description about new permission") + (options, args) = parser.parse_args(args) + if len(args) < 2: + parser.error("Please specify a permission and at least one user") + activate_session(session, goptions) + perm = args[0] + names = args[1:] + users = [] + for n in names: + user = session.getUser(n) + if user is None: + parser.error("No such user: %s" % n) + users.append(user) + kwargs = {} + if options.new: + kwargs['create'] = True + if options.description: + kwargs['description'] = options.description + if options.description and not options.new: + parser.error("Option new must be specified with option description.") + for user in users: + session.grantPermission(user['name'], perm, **kwargs) diff --git a/cli/koji_cli/commands/hostinfo.py b/cli/koji_cli/commands/hostinfo.py new file mode 100644 index 0000000..b563372 --- /dev/null +++ b/cli/koji_cli/commands/hostinfo.py @@ -0,0 +1,80 @@ +from __future__ import absolute_import, division + +from optparse import OptionParser + +from six.moves import range + +import koji + +from koji_cli.lib import ( + ensure_connection, + error, + get_usage_str, + warn +) + + +def anon_handle_hostinfo(goptions, session, args): + "[info] Print basic information about a host" + usage = "usage: %prog hostinfo [options] [ ...]" + parser = OptionParser(usage=get_usage_str(usage)) + (options, args) = parser.parse_args(args) + if len(args) < 1: + parser.error("Please specify a host") + ensure_connection(session, goptions) + error_hit = False + for host in args: + if host.isdigit(): + host = int(host) + info = session.getHost(host) + if info is None: + warn("No such host: %s\n" % host) + error_hit = True + continue + print("Name: %(name)s" % info) + print("ID: %(id)d" % info) + print("Arches: %(arches)s" % info) + print("Capacity: %(capacity)s" % info) + print("Task Load: %(task_load).2f" % info) + if info['description']: + description = info['description'].splitlines() + print("Description: %s" % description[0]) + for line in description[1:]: + print("%s%s" % (" " * 13, line)) + else: + print("Description:") + if info['comment']: + comment = info['comment'].splitlines() + print("Comment: %s" % comment[0]) + for line in comment[1:]: + print("%s%s" % (" " * 9, line)) + else: + print("Comment:") + print("Enabled: %s" % (info['enabled'] and 'yes' or 'no')) + print("Ready: %s" % (info['ready'] and 'yes' or 'no')) + try: + update = session.getLastHostUpdate(info['id'], ts=True) + except koji.ParameterError: + # Hubs prior to v1.25.0 do not have a "ts" parameter for getLastHostUpdate + update = session.getLastHostUpdate(info['id']) + if update is None: + update = "never" + else: + update = koji.formatTimeLong(update) + print("Last Update: %s" % update) + print("Channels: %s" % ' '.join([c['name'] + for c in session.listChannels(hostID=info['id'])])) + print("Active Buildroots:") + states = {0: "INIT", 1: "WAITING", 2: "BUILDING"} + rows = [('NAME', 'STATE', 'CREATION TIME')] + for s in range(0, 3): + for b in session.listBuildroots(hostID=info['id'], state=s): + rows.append((("%s-%s-%s" % (b['tag_name'], b['id'], b['repo_id'])), states[s], + b['create_event_time'][:b['create_event_time'].find('.')])) + if len(rows) > 1: + for row in rows: + print("%-50s %-10s %-20s" % row) + else: + print("None") + if error_hit: + error() diff --git a/cli/koji_cli/commands/image_build.py b/cli/koji_cli/commands/image_build.py new file mode 100644 index 0000000..c9a6793 --- /dev/null +++ b/cli/koji_cli/commands/image_build.py @@ -0,0 +1,198 @@ +from __future__ import absolute_import, division + +import os +from optparse import OptionParser + +import koji + +from koji_cli.lib import ( + _progress_callback, + _running_in_bg, + activate_session, + get_usage_str, + unique_path, + watch_tasks +) + + +def handle_image_build(options, session, args): + """[build] Create a disk image given an install tree""" + formats = ('vmdk', 'qcow', 'qcow2', 'vdi', 'vpc', 'rhevm-ova', + 'vsphere-ova', 'vagrant-virtualbox', 'vagrant-libvirt', + 'vagrant-vmware-fusion', 'vagrant-hyperv', 'docker', 'raw-xz', + 'liveimg-squashfs', 'tar-gz') + usage = "usage: %prog image-build [options] " \ + " [ ...]" + usage += "\n %prog image-build --config \n" + parser = OptionParser(usage=get_usage_str(usage)) + parser.add_option("--background", action="store_true", + help="Run the image creation task at a lower priority") + parser.add_option("--config", + help="Use a configuration file to define image-build options " + "instead of command line options (they will be ignored).") + parser.add_option("--disk-size", default=10, help="Set the disk device size in gigabytes") + parser.add_option("--distro", + help="specify the RPM based distribution the image will be based " + "on with the format RHEL-X.Y, CentOS-X.Y, SL-X.Y, or Fedora-NN. " + "The packages for the Distro you choose must have been built " + "in this system.") + parser.add_option("--format", default=[], action="append", + help="Convert results to one or more formats " + "(%s), this option may be used " + "multiple times. By default, specifying this option will " + "omit the raw disk image (which is 10G in size) from the " + "build results. If you really want it included with converted " + "images, pass in 'raw' as an option." % ', '.join(formats)) + parser.add_option("--kickstart", help="Path to a local kickstart file") + parser.add_option("--ksurl", metavar="SCMURL", + help="The URL to the SCM containing the kickstart file") + parser.add_option("--ksversion", metavar="VERSION", + help="The syntax version used in the kickstart file") + parser.add_option("--noprogress", action="store_true", + help="Do not display progress of the upload") + parser.add_option("--noverifyssl", action="store_true", + help="Use the noverifyssl option for the install tree and all repos. " + "This option is only allowed if enabled on the builder.") + parser.add_option("--nowait", action="store_false", dest="wait", + help="Don't wait on image creation") + parser.add_option("--ova-option", action="append", + help="Override a value in the OVA description XML. Provide a value " + "in a name=value format, such as 'ovf_memory_mb=6144'") + parser.add_option("--factory-parameter", nargs=2, action="append", + help="Pass a parameter to Image Factory. The results are highly specific " + "to the image format being created. This is a two argument parameter " + "that can be specified an arbitrary number of times. For example: " + "--factory-parameter docker_cmd '[ \"/bin/echo Hello World\" ]'") + parser.add_option("--release", help="Forcibly set the release field") + parser.add_option("--repo", action="append", + help="Specify a repo that will override the repo used to install " + "RPMs in the image. May be used multiple times. The " + "build tag repo associated with the target is the default.") + parser.add_option("--scratch", action="store_true", help="Create a scratch image") + parser.add_option("--skip-tag", action="store_true", help="Do not attempt to tag package") + parser.add_option("--can-fail", action="store", dest="optional_arches", + metavar="ARCH1,ARCH2,...", default="", + help="List of archs which are not blocking for build (separated by commas.") + parser.add_option("--specfile", metavar="URL", + help="SCM URL to spec file fragment to use to generate wrapper RPMs") + parser.add_option("--wait", action="store_true", + help="Wait on the image creation, even if running in the background") + + (task_options, args) = parser.parse_args(args) + + if task_options.config: + section = 'image-build' + config = koji.read_config_files([(task_options.config, True)]) + if not config.has_section(section): + parser.error("single section called [%s] is required" % section) + # pluck out the positional arguments first + args = [] + for arg in ('name', 'version', 'target', 'install_tree'): + args.append(config.get(section, arg)) + config.remove_option(section, arg) + args.extend(config.get(section, 'arches').split(',')) + config.remove_option(section, 'arches') + # turn comma-separated options into lists + for arg in ('repo', 'format'): + if config.has_option(section, arg): + setattr(task_options, arg, config.get(section, arg).split(',')) + config.remove_option(section, arg) + if config.has_option(section, 'can_fail'): + setattr(task_options, 'optional_arches', config.get(section, 'can_fail').split(',')) + config.remove_option(section, 'can_fail') + # handle everything else + for k, v in config.items(section): + setattr(task_options, k, v) + + # ova-options belong in their own section + section = 'ova-options' + if config.has_section(section): + task_options.ova_option = [] + for k, v in config.items(section): + task_options.ova_option.append('%s=%s' % (k, v)) + + # as do factory-parameters + section = 'factory-parameters' + if config.has_section(section): + task_options.factory_parameter = [] + for k, v in config.items(section): + # We do this, rather than a dict, to match what argparse spits out + task_options.factory_parameter.append((k, v)) + + else: + if len(args) < 5: + parser.error("At least five arguments are required: a name, a version, " + "a build target, a URL to an install tree, and 1 or more architectures.") + if not task_options.ksurl and not task_options.kickstart: + parser.error('You must specify --kickstart') + if not task_options.distro: + parser.error( + "You must specify --distro. Examples: Fedora-16, RHEL-6.4, SL-6.4 or CentOS-6.4") + return _build_image_oz(options, task_options, session, args) + + +def _build_image_oz(options, task_opts, session, args): + """ + A private helper function that houses common CLI code for building + images with Oz and ImageFactory + """ + activate_session(session, options) + + # Set the task's priority. Users can only lower it with --background. + priority = None + if task_opts.background: + # relative to koji.PRIO_DEFAULT; higher means a "lower" priority. + priority = 5 + if _running_in_bg() or task_opts.noprogress: + callback = None + else: + callback = _progress_callback + + # We do some early sanity checking of the given target. + # Kojid gets these values again later on, but we check now as a convenience + # for the user. + target = args[2] + tmp_target = session.getBuildTarget(target) + if not tmp_target: + raise koji.GenericError("No such build target: %s" % target) + dest_tag = session.getTag(tmp_target['dest_tag']) + if not dest_tag: + raise koji.GenericError("No such destination tag: %s" % tmp_target['dest_tag_name']) + + # Set the architectures + arches = [] + for arch in args[4:]: + arches.append(koji.canonArch(arch)) + + # Upload the KS file to the staging area. + # If it's a URL, it's kojid's job to go get it when it does the checkout. + if not task_opts.ksurl: + if not task_opts.scratch: + # only scratch builds can omit ksurl + raise koji.GenericError("Non-scratch builds must provide ksurl") + ksfile = task_opts.kickstart + serverdir = unique_path('cli-image') + session.uploadWrapper(ksfile, serverdir, callback=callback) + task_opts.kickstart = os.path.join('work', serverdir, + os.path.basename(ksfile)) + print('') + + hub_opts = {} + for opt in ('ksurl', 'ksversion', 'kickstart', 'scratch', 'repo', + 'release', 'skip_tag', 'specfile', 'distro', 'format', + 'disk_size', 'ova_option', 'factory_parameter', + 'optional_arches', 'noverifyssl'): + val = getattr(task_opts, opt, None) + if val is not None: + hub_opts[opt] = val + # finally, create the task. + task_id = session.buildImageOz(args[0], args[1], arches, target, args[3], + opts=hub_opts, priority=priority) + + if not options.quiet: + print("Created task: %d" % task_id) + print("Task info: %s/taskinfo?taskID=%s" % (options.weburl, task_id)) + if task_opts.wait or (task_opts.wait is None and not _running_in_bg()): + session.logout() + return watch_tasks(session, [task_id], quiet=options.quiet, + poll_interval=options.poll_interval, topurl=options.topurl) diff --git a/cli/koji_cli/commands/image_build_indirection.py b/cli/koji_cli/commands/image_build_indirection.py new file mode 100644 index 0000000..aa256e1 --- /dev/null +++ b/cli/koji_cli/commands/image_build_indirection.py @@ -0,0 +1,148 @@ +from __future__ import absolute_import, division + +import os +from optparse import OptionParser + +import koji + +from koji_cli.lib import ( + _progress_callback, + _running_in_bg, + activate_session, + get_usage_str, + unique_path, + watch_tasks +) + + +def handle_image_build_indirection(options, session, args): + """[build] Create a disk image using other disk images via the Indirection plugin""" + usage = "usage: %prog image-build-indirection [base_image] [utility_image] " \ + "[indirection_build_template]" + usage += "\n %prog image-build --config \n" + parser = OptionParser(usage=get_usage_str(usage)) + parser.add_option("--config", + help="Use a configuration file to define image-build options " + "instead of command line options (they will be ignored).") + parser.add_option("--background", action="store_true", + help="Run the image creation task at a lower priority") + parser.add_option("--name", help="Name of the output image") + parser.add_option("--version", help="Version of the output image") + parser.add_option("--release", help="Release of the output image") + parser.add_option("--arch", help="Architecture of the output image and input images") + parser.add_option("--target", help="Build target to use for the indirection build") + parser.add_option("--skip-tag", action="store_true", help="Do not tag the resulting build") + parser.add_option("--base-image-task", + help="ID of the createImage task of the base image to be used") + parser.add_option("--base-image-build", help="NVR or build ID of the base image to be used") + parser.add_option("--utility-image-task", + help="ID of the createImage task of the utility image to be used") + parser.add_option("--utility-image-build", + help="NVR or build ID of the utility image to be used") + parser.add_option("--indirection-template", + help="Name of the local file, or SCM file containing the template used to " + "drive the indirection plugin") + parser.add_option("--indirection-template-url", + help="SCM URL containing the template used to drive the indirection plugin") + parser.add_option("--results-loc", + help="Relative path inside the working space image where the results " + "should be extracted from") + parser.add_option("--scratch", action="store_true", help="Create a scratch image") + parser.add_option("--wait", action="store_true", + help="Wait on the image creation, even if running in the background") + parser.add_option("--nowait", action="store_false", dest="wait", + help="Do not wait on the image creation") + parser.add_option("--noprogress", action="store_true", + help="Do not display progress of the upload") + + (task_options, args) = parser.parse_args(args) + _build_image_indirection(options, task_options, session, args) + + +def _build_image_indirection(options, task_opts, session, args): + """ + A private helper function for builds using the indirection plugin of ImageFactory + """ + + # Do some sanity checks before even attempting to create the session + if not (bool(task_opts.utility_image_task) != + bool(task_opts.utility_image_build)): + raise koji.GenericError("You must specify either a utility-image task or build ID/NVR") + + if not (bool(task_opts.base_image_task) != + bool(task_opts.base_image_build)): + raise koji.GenericError("You must specify either a base-image task or build ID/NVR") + + required_opts = ['name', 'version', 'arch', 'target', 'indirection_template', 'results_loc'] + optional_opts = ['indirection_template_url', 'scratch', 'utility_image_task', + 'utility_image_build', 'base_image_task', 'base_image_build', 'release', + 'skip_tag'] + + missing = [] + for opt in required_opts: + if not getattr(task_opts, opt, None): + missing.append(opt) + + if len(missing) > 0: + print("Missing the following required options: %s" % + ' '.join(['--%s' % o.replace('_', '-') for o in missing])) + raise koji.GenericError("Missing required options specified above") + + activate_session(session, options) + + # Set the task's priority. Users can only lower it with --background. + priority = None + if task_opts.background: + # relative to koji.PRIO_DEFAULT; higher means a "lower" priority. + priority = 5 + if _running_in_bg() or task_opts.noprogress: + callback = None + else: + callback = _progress_callback + + # We do some early sanity checking of the given target. + # Kojid gets these values again later on, but we check now as a convenience + # for the user. + + tmp_target = session.getBuildTarget(task_opts.target) + if not tmp_target: + raise koji.GenericError("No such build target: %s" % tmp_target) + dest_tag = session.getTag(tmp_target['dest_tag']) + if not dest_tag: + raise koji.GenericError("No such destination tag: %s" % tmp_target['dest_tag_name']) + + # Set the architecture + task_opts.arch = koji.canonArch(task_opts.arch) + + # Upload the indirection template file to the staging area. + # If it's a URL, it's kojid's job to go get it when it does the checkout. + if not task_opts.indirection_template_url: + if not task_opts.scratch: + # only scratch builds can omit indirection_template_url + raise koji.GenericError( + "Non-scratch builds must provide a URL for the indirection template") + templatefile = task_opts.indirection_template + serverdir = unique_path('cli-image-indirection') + session.uploadWrapper(templatefile, serverdir, callback=callback) + task_opts.indirection_template = os.path.join('work', serverdir, + os.path.basename(templatefile)) + print('') + + hub_opts = {} + # Just pass everything in as opts. No posiitonal arguments at all. Why not? + for opt in required_opts + optional_opts: + val = getattr(task_opts, opt, None) + # We pass these through even if they are None + # The builder code can then check if they are set without using getattr + hub_opts[opt] = val + + # finally, create the task. + task_id = session.buildImageIndirection(opts=hub_opts, + priority=priority) + + if not options.quiet: + print("Created task: %d" % task_id) + print("Task info: %s/taskinfo?taskID=%s" % (options.weburl, task_id)) + if task_opts.wait or (task_opts.wait is None and not _running_in_bg()): + session.logout() + return watch_tasks(session, [task_id], quiet=options.quiet, topurl=options.topurl) diff --git a/cli/koji_cli/commands/import.py b/cli/koji_cli/commands/import.py new file mode 100644 index 0000000..5af2ad4 --- /dev/null +++ b/cli/koji_cli/commands/import.py @@ -0,0 +1,163 @@ +from __future__ import absolute_import, division + +import os +import sys +from optparse import OptionParser + +import koji +from koji.util import to_list + +from koji_cli.lib import ( + activate_session, + get_usage_str, + linked_upload, + unique_path, + warn +) + + +def handle_import(goptions, session, args): + "[admin] Import externally built RPMs into the database" + usage = "usage: %prog import [options] [ ...]" + parser = OptionParser(usage=get_usage_str(usage)) + parser.add_option("--link", action="store_true", + help="Attempt to hardlink instead of uploading") + parser.add_option("--test", action="store_true", help="Don't actually import") + parser.add_option("--create-build", action="store_true", help="Auto-create builds as needed") + parser.add_option("--src-epoch", help="When auto-creating builds, use this epoch") + (options, args) = parser.parse_args(args) + if len(args) < 1: + parser.error("At least one package must be specified") + if options.src_epoch in ('None', 'none', '(none)'): + options.src_epoch = None + elif options.src_epoch: + try: + options.src_epoch = int(options.src_epoch) + except (ValueError, TypeError): + parser.error("Invalid value for epoch: %s" % options.src_epoch) + activate_session(session, goptions) + to_import = {} + for path in args: + data = koji.get_header_fields(path, ('name', 'version', 'release', 'epoch', + 'arch', 'sigmd5', 'sourcepackage', 'sourcerpm')) + if data['sourcepackage']: + data['arch'] = 'src' + nvr = "%(name)s-%(version)s-%(release)s" % data + else: + nvr = "%(name)s-%(version)s-%(release)s" % koji.parse_NVRA(data['sourcerpm']) + to_import.setdefault(nvr, []).append((path, data)) + builds_missing = False + nvrs = sorted(to_list(to_import.keys())) + for nvr in nvrs: + to_import[nvr].sort() + for path, data in to_import[nvr]: + if data['sourcepackage']: + break + else: + # no srpm included, check for build + binfo = session.getBuild(nvr) + if not binfo: + print("Missing build or srpm: %s" % nvr) + builds_missing = True + if builds_missing and not options.create_build: + print("Aborting import") + return + + # local function to help us out below + def do_import(path, data): + rinfo = dict([(k, data[k]) for k in ('name', 'version', 'release', 'arch')]) + prev = session.getRPM(rinfo) + if prev and not prev.get('external_repo_id', 0): + if prev['payloadhash'] == koji.hex_string(data['sigmd5']): + print("RPM already imported: %s" % path) + else: + warn("md5sum mismatch for %s" % path) + warn(" A different rpm with the same name has already been imported") + warn(" Existing sigmd5 is %r, your import has %r" % ( + prev['payloadhash'], koji.hex_string(data['sigmd5']))) + print("Skipping import") + return + if options.test: + print("Test mode -- skipping import for %s" % path) + return + serverdir = unique_path('cli-import') + if options.link: + linked_upload(path, serverdir) + else: + sys.stdout.write("uploading %s... " % path) + sys.stdout.flush() + session.uploadWrapper(path, serverdir) + print("done") + sys.stdout.flush() + sys.stdout.write("importing %s... " % path) + sys.stdout.flush() + try: + session.importRPM(serverdir, os.path.basename(path)) + except koji.GenericError as e: + print("\nError importing: %s" % str(e).splitlines()[-1]) + sys.stdout.flush() + else: + print("done") + sys.stdout.flush() + + for nvr in nvrs: + # check for existing build + need_build = True + binfo = session.getBuild(nvr) + if binfo: + b_state = koji.BUILD_STATES[binfo['state']] + if b_state == 'COMPLETE': + need_build = False + elif b_state in ['FAILED', 'CANCELED']: + if not options.create_build: + print("Build %s state is %s. Skipping import" % (nvr, b_state)) + continue + else: + print("Build %s exists with state=%s. Skipping import" % (nvr, b_state)) + continue + + # import srpms first, if any + for path, data in to_import[nvr]: + if data['sourcepackage']: + if binfo and b_state != 'COMPLETE': + # need to fix the state + print("Creating empty build: %s" % nvr) + b_data = koji.util.dslice(binfo, ['name', 'version', 'release']) + b_data['epoch'] = data['epoch'] + session.createEmptyBuild(**b_data) + binfo = session.getBuild(nvr) + do_import(path, data) + need_build = False + + if need_build: + # if we're doing this here, we weren't given the matching srpm + if not options.create_build: # pragma: no cover + if binfo: + # should have caught this earlier, but just in case... + b_state = koji.BUILD_STATES[binfo['state']] + print("Build %s state is %s. Skipping import" % (nvr, b_state)) + continue + else: + print("No such build: %s (include matching srpm or use " + "--create-build option to add it)" % nvr) + continue + else: + # let's make a new build + b_data = koji.parse_NVR(nvr) + if options.src_epoch: + b_data['epoch'] = options.src_epoch + else: + # pull epoch from first rpm + data = to_import[nvr][0][1] + b_data['epoch'] = data['epoch'] + if options.test: + print("Test mode -- would have created empty build: %s" % nvr) + else: + print("Creating empty build: %s" % nvr) + session.createEmptyBuild(**b_data) + binfo = session.getBuild(nvr) + + for path, data in to_import[nvr]: + if data['sourcepackage']: + continue + do_import(path, data) diff --git a/cli/koji_cli/commands/import_archive.py b/cli/koji_cli/commands/import_archive.py new file mode 100644 index 0000000..6cc4ee7 --- /dev/null +++ b/cli/koji_cli/commands/import_archive.py @@ -0,0 +1,115 @@ +from __future__ import absolute_import, division + +import os +from optparse import OptionParser + +import koji + +from koji_cli.lib import ( + _progress_callback, + _running_in_bg, + activate_session, + arg_filter, + get_usage_str, + linked_upload, + unique_path +) + + +def handle_import_archive(options, session, args): + "[admin] Import an archive file and associate it with a build" + usage = "usage: %prog import-archive [ 1: + parser.error("You must specify a build ID or N-V-R and an archive to import") + + activate_session(session, options) + + if not suboptions.type: + parser.error("You must specify an archive type") + if suboptions.type == 'maven': + if not (session.hasPerm('maven-import') or session.hasPerm('admin')): + parser.error("This action requires the maven-import privilege") + if not suboptions.type_info: + parser.error("--type-info must point to a .pom file when importing Maven archives") + pom_info = koji.parse_pom(suboptions.type_info) + maven_info = koji.pom_to_maven_info(pom_info) + suboptions.type_info = maven_info + elif suboptions.type == 'win': + if not (session.hasPerm('win-import') or session.hasPerm('admin')): + parser.error("This action requires the win-import privilege") + if not suboptions.type_info: + parser.error("--type-info must be specified") + type_info = suboptions.type_info.split(':', 2) + if len(type_info) < 2: + parser.error("--type-info must be in relpath:platforms[:flags] format") + win_info = {'relpath': type_info[0], 'platforms': type_info[1].split()} + if len(type_info) > 2: + win_info['flags'] = type_info[2].split() + else: + win_info['flags'] = [] + suboptions.type_info = win_info + elif suboptions.type == 'image': + if not (session.hasPerm('image-import') or session.hasPerm('admin')): + parser.error("This action requires the image-import privilege") + if not suboptions.type_info: + parser.error("--type-info must be specified") + image_info = {'arch': suboptions.type_info} + suboptions.type_info = image_info + else: + parser.error("Unsupported archive type: %s" % suboptions.type) + + buildinfo = session.getBuild(arg_filter(args[0])) + if not buildinfo: + if not suboptions.create_build: + parser.error("No such build: %s" % args[0]) + buildinfo = koji.parse_NVR(args[0]) + if buildinfo['epoch'] == '': + buildinfo['epoch'] = None + else: + buildinfo['epoch'] = int(buildinfo['epoch']) + if suboptions.type == 'maven': + # --type-info should point to a local .pom file + session.createMavenBuild(buildinfo, suboptions.type_info) + elif suboptions.type == 'win': + # We're importing, so we don't know what platform the build + # was run on. Use "import" as a placeholder. + session.createWinBuild(buildinfo, {'platform': 'import'}) + elif suboptions.type == 'image': + # --type-info should have an arch of the image + session.createImageBuild(buildinfo) + else: + # should get caught above + assert False # pragma: no cover + + for filepath in args[1:]: + filename = os.path.basename(filepath) + print("Uploading archive: %s" % filename) + serverdir = unique_path('cli-import') + if _running_in_bg() or suboptions.noprogress: + callback = None + else: + callback = _progress_callback + if suboptions.link: + linked_upload(filepath, serverdir) + else: + session.uploadWrapper(filepath, serverdir, callback=callback) + print('') + serverpath = "%s/%s" % (serverdir, filename) + session.importArchive(serverpath, buildinfo, suboptions.type, suboptions.type_info) + print("Imported: %s" % filename) diff --git a/cli/koji_cli/commands/import_cg.py b/cli/koji_cli/commands/import_cg.py new file mode 100644 index 0000000..ce9d4eb --- /dev/null +++ b/cli/koji_cli/commands/import_cg.py @@ -0,0 +1,68 @@ +from __future__ import absolute_import, division + +import os +from optparse import OptionParser + +import koji + +from koji_cli.lib import ( + _progress_callback, + _running_in_bg, + activate_session, + error, + get_usage_str, + linked_upload, + unique_path +) + + +def handle_import_cg(goptions, session, args): + "[admin] Import external builds with rich metadata" + usage = "usage: %prog import-cg [options] " + parser = OptionParser(usage=get_usage_str(usage)) + parser.add_option("--noprogress", action="store_true", + help="Do not display progress of the upload") + parser.add_option("--link", action="store_true", + help="Attempt to hardlink instead of uploading") + parser.add_option("--test", action="store_true", help="Don't actually import") + parser.add_option("--token", action="store", default=None, help="Build reservation token") + (options, args) = parser.parse_args(args) + if len(args) < 2: + parser.error("Please specify metadata files directory") + activate_session(session, goptions) + metadata = koji.load_json(args[0]) + if 'output' not in metadata: + error("Metadata contains no output") + localdir = args[1] + + to_upload = [] + for info in metadata['output']: + if info.get('metadata_only', False): + continue + localpath = os.path.join(localdir, info.get('relpath', ''), info['filename']) + if not os.path.exists(localpath): + parser.error("No such file: %s" % localpath) + to_upload.append([localpath, info]) + + if options.test: + return + + # get upload path + # XXX - need a better way + serverdir = unique_path('cli-import') + + for localpath, info in to_upload: + relpath = os.path.join(serverdir, info.get('relpath', '')) + if _running_in_bg() or options.noprogress: + callback = None + else: + callback = _progress_callback + if options.link: + linked_upload(localpath, relpath) + else: + print("Uploading %s" % localpath) + session.uploadWrapper(localpath, relpath, callback=callback) + if callback: + print('') + + session.CGImport(metadata, serverdir, options.token) diff --git a/cli/koji_cli/commands/import_comps.py b/cli/koji_cli/commands/import_comps.py new file mode 100644 index 0000000..5f0f06c --- /dev/null +++ b/cli/koji_cli/commands/import_comps.py @@ -0,0 +1,113 @@ +from __future__ import absolute_import, division + +from optparse import OptionParser + +import six +import six.moves.xmlrpc_client + + +from koji_cli.lib import ( + activate_session, + error, + get_usage_str +) + +try: + import libcomps +except ImportError: # pragma: no cover + libcomps = None + try: + import yum.comps as yumcomps + except ImportError: + yumcomps = None + + +def handle_import_comps(goptions, session, args): + "Import group/package information from a comps file" + usage = "usage: %prog import-comps [options] " + parser = OptionParser(usage=get_usage_str(usage)) + parser.add_option("--force", action="store_true", help="force import") + (local_options, args) = parser.parse_args(args) + if len(args) != 2: + parser.error("Incorrect number of arguments") + activate_session(session, goptions) + # check if the tag exists + dsttag = session.getTag(args[1]) + if dsttag is None: + error("No such tag: %s" % args[1]) + if libcomps is not None: + _import_comps(session, args[0], args[1], local_options) + elif yumcomps is not None: + _import_comps_alt(session, args[0], args[1], local_options) + else: + error("comps module not available") + + +def _import_comps(session, filename, tag, options): + """Import comps data using libcomps module""" + comps = libcomps.Comps() + comps.fromxml_f(filename) + force = options.force + ptypes = { + libcomps.PACKAGE_TYPE_DEFAULT: 'default', + libcomps.PACKAGE_TYPE_OPTIONAL: 'optional', + libcomps.PACKAGE_TYPE_CONDITIONAL: 'conditional', + libcomps.PACKAGE_TYPE_MANDATORY: 'mandatory', + libcomps.PACKAGE_TYPE_UNKNOWN: 'unknown', + } + for group in comps.groups: + print("Group: %s (%s)" % (group.id, group.name)) + session.groupListAdd( + tag, group.id, force=force, display_name=group.name, + is_default=bool(group.default), + uservisible=bool(group.uservisible), + description=group.desc, + langonly=group.lang_only, + biarchonly=bool(group.biarchonly)) + for pkg in group.packages: + pkgopts = {'type': ptypes[pkg.type], + 'basearchonly': bool(pkg.basearchonly), + } + if pkg.type == libcomps.PACKAGE_TYPE_CONDITIONAL: + pkgopts['requires'] = pkg.requires + for k in pkgopts.keys(): + if six.PY2 and isinstance(pkgopts[k], unicode): # noqa: F821 + pkgopts[k] = str(pkgopts[k]) + s_opts = ', '.join(["'%s': %r" % (k, pkgopts[k]) for k in sorted(pkgopts.keys())]) + print(" Package: %s: {%s}" % (pkg.name, s_opts)) + session.groupPackageListAdd(tag, group.id, pkg.name, force=force, **pkgopts) + # libcomps does not support group dependencies + # libcomps does not support metapkgs + + +def _import_comps_alt(session, filename, tag, options): # no cover 3.x + """Import comps data using yum.comps module""" + print('WARN: yum.comps does not support the biarchonly of group and basearchonly of package') + comps = yumcomps.Comps() + comps.add(filename) + force = options.force + for group in comps.groups: + print("Group: %(groupid)s (%(name)s)" % vars(group)) + session.groupListAdd(tag, group.groupid, force=force, display_name=group.name, + is_default=bool(group.default), + uservisible=bool(group.user_visible), + description=group.description, + langonly=group.langonly) + # yum.comps does not support the biarchonly field + for ptype, pdata in [('mandatory', group.mandatory_packages), + ('default', group.default_packages), + ('optional', group.optional_packages), + ('conditional', group.conditional_packages)]: + for pkg in pdata: + # yum.comps does not support basearchonly + pkgopts = {'type': ptype} + if ptype == 'conditional': + pkgopts['requires'] = pdata[pkg] + for k in pkgopts.keys(): + if six.PY2 and isinstance(pkgopts[k], unicode): # noqa: F821 + pkgopts[k] = str(pkgopts[k]) + s_opts = ', '.join(["'%s': %r" % (k, pkgopts[k]) for k in sorted(pkgopts.keys())]) + print(" Package: %s: {%s}" % (pkg, s_opts)) + session.groupPackageListAdd(tag, group.groupid, pkg, force=force, **pkgopts) + # yum.comps does not support group dependencies + # yum.comps does not support metapkgs diff --git a/cli/koji_cli/commands/import_sig.py b/cli/koji_cli/commands/import_sig.py new file mode 100644 index 0000000..8a44799 --- /dev/null +++ b/cli/koji_cli/commands/import_sig.py @@ -0,0 +1,82 @@ +from __future__ import absolute_import, division + +import os +from optparse import SUPPRESS_HELP, OptionParser + + +import koji +from koji.util import base64encode, md5_constructor + +from koji_cli.lib import ( + activate_session, + get_usage_str, + warn +) + + +def handle_import_sig(goptions, session, args): + "[admin] Import signatures into the database and write signed RPMs" + usage = "usage: %prog import-sig [options] [ ...]" + parser = OptionParser(usage=get_usage_str(usage)) + parser.add_option("--with-unsigned", action="store_true", + help="Also import unsigned sig headers") + parser.add_option("--write", action="store_true", help=SUPPRESS_HELP) + parser.add_option("--test", action="store_true", help="Test mode -- don't actually import") + (options, args) = parser.parse_args(args) + if len(args) < 1: + parser.error("At least one package must be specified") + for path in args: + if not os.path.exists(path): + parser.error("No such file: %s" % path) + activate_session(session, goptions) + for path in args: + data = koji.get_header_fields(path, ('name', 'version', 'release', 'arch', 'siggpg', + 'sigpgp', 'dsaheader', 'rsaheader', + 'sourcepackage')) + if data['sourcepackage']: + data['arch'] = 'src' + sigkey = data['siggpg'] + if not sigkey: + sigkey = data['sigpgp'] + if not sigkey: + sigkey = data['dsaheader'] + if not sigkey: + sigkey = data['rsaheader'] + if not sigkey: + sigkey = "" + if not options.with_unsigned: + print("Skipping unsigned package: %s" % path) + continue + else: + sigkey = koji.get_sigpacket_key_id(sigkey) + del data['siggpg'] + del data['sigpgp'] + del data['dsaheader'] + del data['rsaheader'] + rinfo = session.getRPM(data) + if not rinfo: + print("No such rpm in system: %(name)s-%(version)s-%(release)s.%(arch)s" % data) + continue + if rinfo.get('external_repo_id'): + print("Skipping external rpm: %(name)s-%(version)s-%(release)s.%(arch)s@" + "%(external_repo_name)s" % rinfo) + continue + sighdr = koji.rip_rpm_sighdr(path) + previous = session.queryRPMSigs(rpm_id=rinfo['id'], sigkey=sigkey) + assert len(previous) <= 1 + if previous: + sighash = md5_constructor(sighdr).hexdigest() + if previous[0]['sighash'] == sighash: + print("Signature already imported: %s" % path) + continue + else: + warn("signature mismatch: %s" % path) + warn(" The system already has a signature for this rpm with key %s" % sigkey) + warn(" The two signature headers are not the same") + continue + print("Importing signature [key %s] from %s..." % (sigkey, path)) + if not options.test: + session.addRPMSig(rinfo['id'], base64encode(sighdr)) + print("Writing signed copy") + if not options.test: + session.writeSignedRPM(rinfo['id'], sigkey) diff --git a/cli/koji_cli/commands/latest_build.py b/cli/koji_cli/commands/latest_build.py new file mode 100644 index 0000000..0503b94 --- /dev/null +++ b/cli/koji_cli/commands/latest_build.py @@ -0,0 +1,101 @@ +from __future__ import absolute_import, division + +import os +import textwrap +from optparse import OptionParser + +import koji + +from koji_cli.lib import ( + ensure_connection, + get_usage_str +) + + +def anon_handle_latest_build(goptions, session, args): + """[info] Print the latest builds for a tag""" + usage = """\ + usage: %prog latest-build [options] [ ...] + + The first option should be the name of a tag, not the name of a build target. + If you want to know the latest build in buildroots for a given build target, + then you should use the name of the build tag for that target. You can find + this value by running '%prog list-targets --name=' + + More information on tags and build targets can be found in the documentation. + https://docs.pagure.org/koji/HOWTO/#package-organization""" + + usage = textwrap.dedent(usage) + parser = OptionParser(usage=get_usage_str(usage)) + parser.add_option("--arch", help="List all of the latest packages for this arch") + parser.add_option("--all", action="store_true", + help="List all of the latest packages for this tag") + parser.add_option("--quiet", action="store_true", default=goptions.quiet, + help="Do not print the header information") + parser.add_option("--paths", action="store_true", help="Show the file paths") + parser.add_option("--type", + help="Show builds of the given type only. " + "Currently supported types: maven, win, image, or any custom " + "content generator btypes") + (options, args) = parser.parse_args(args) + if len(args) == 0: + parser.error("A tag name must be specified") + ensure_connection(session, goptions) + if options.all: + if len(args) > 1: + parser.error("A package name may not be combined with --all") + # Set None as the package argument + args.append(None) + else: + if len(args) < 2: + parser.error("A tag name and package name must be specified") + pathinfo = koji.PathInfo() + + for pkg in args[1:]: + if options.arch: + rpms, builds = session.getLatestRPMS(args[0], package=pkg, arch=options.arch) + builds_hash = dict([(x['build_id'], x) for x in builds]) + data = rpms + if options.paths: + for x in data: + z = x.copy() + x['name'] = builds_hash[x['build_id']]['package_name'] + x['path'] = os.path.join(pathinfo.build(x), pathinfo.rpm(z)) + fmt = "%(path)s" + else: + fmt = "%(name)s-%(version)s-%(release)s.%(arch)s" + else: + kwargs = {'package': pkg} + if options.type: + kwargs['type'] = options.type + data = session.getLatestBuilds(args[0], **kwargs) + if options.paths: + if options.type == 'maven': + for x in data: + x['path'] = pathinfo.mavenbuild(x) + fmt = "%(path)-40s %(tag_name)-20s %(maven_group_id)-20s " \ + "%(maven_artifact_id)-20s %(owner_name)s" + else: + for x in data: + x['path'] = pathinfo.build(x) + fmt = "%(path)-40s %(tag_name)-20s %(owner_name)s" + else: + if options.type == 'maven': + fmt = "%(nvr)-40s %(tag_name)-20s %(maven_group_id)-20s " \ + "%(maven_artifact_id)-20s %(owner_name)s" + else: + fmt = "%(nvr)-40s %(tag_name)-20s %(owner_name)s" + if not options.quiet: + if options.type == 'maven': + print("%-40s %-20s %-20s %-20s %s" % + ("Build", "Tag", "Group Id", "Artifact Id", "Built by")) + print("%s %s %s %s %s" % + ("-" * 40, "-" * 20, "-" * 20, "-" * 20, "-" * 16)) + else: + print("%-40s %-20s %s" % ("Build", "Tag", "Built by")) + print("%s %s %s" % ("-" * 40, "-" * 20, "-" * 16)) + options.quiet = True + + output = sorted([fmt % x for x in data]) + for line in output: + print(line) diff --git a/cli/koji_cli/commands/list_api.py b/cli/koji_cli/commands/list_api.py new file mode 100644 index 0000000..da939d1 --- /dev/null +++ b/cli/koji_cli/commands/list_api.py @@ -0,0 +1,41 @@ +from __future__ import absolute_import, division + +from optparse import OptionParser + + +from koji_cli.lib import ( + ensure_connection, + get_usage_str +) + + +def anon_handle_list_api(goptions, session, args): + "[info] Print the list of XML-RPC APIs" + usage = "usage: %prog list-api [options] [method_name ...]" + parser = OptionParser(usage=get_usage_str(usage)) + (options, args) = parser.parse_args(args) + ensure_connection(session, goptions) + if args: + for method in args: + help = session.system.methodHelp(method) + if not help: + parser.error("Unknown method: %s" % method) + print(help) + else: + for x in sorted(session._listapi(), key=lambda x: x['name']): + if 'argdesc' in x: + args = x['argdesc'] + elif x['args']: + # older servers may not provide argdesc + expanded = [] + for arg in x['args']: + if isinstance(arg, str): + expanded.append(arg) + else: + expanded.append('%s=%s' % (arg[0], arg[1])) + args = "(%s)" % ", ".join(expanded) + else: + args = "()" + print('%s%s' % (x['name'], args)) + if x['doc']: + print(" description: %s" % x['doc']) diff --git a/cli/koji_cli/commands/list_buildroot.py b/cli/koji_cli/commands/list_buildroot.py new file mode 100644 index 0000000..2db3a60 --- /dev/null +++ b/cli/koji_cli/commands/list_buildroot.py @@ -0,0 +1,38 @@ +from __future__ import absolute_import, division + +from optparse import SUPPRESS_HELP, OptionParser + +from koji_cli.lib import ( + ensure_connection, + get_usage_str +) + + +def anon_handle_list_buildroot(goptions, session, args): + "[info] List the rpms used in or built in a buildroot" + usage = "usage: %prog list-buildroot [options] " + parser = OptionParser(usage=get_usage_str(usage)) + parser.add_option("--paths", action="store_true", help=SUPPRESS_HELP) + parser.add_option("--built", action="store_true", help="Show the built rpms") + parser.add_option("--verbose", "-v", action="store_true", help="Show more information") + (options, args) = parser.parse_args(args) + if len(args) != 1: + parser.error("Incorrect number of arguments") + ensure_connection(session, goptions) + if options.paths: + parser.error("--paths option is deprecated and will be removed in 1.30") + buildrootID = int(args[0]) + opts = {} + if options.built: + opts['buildrootID'] = buildrootID + else: + opts['componentBuildrootID'] = buildrootID + data = session.listRPMs(**opts) + + fmt = "%(nvr)s.%(arch)s" + order = sorted([(fmt % x, x) for x in data]) + for nvra, rinfo in order: + if options.verbose and rinfo.get('is_update'): + print("%s [update]" % nvra) + else: + print(nvra) diff --git a/cli/koji_cli/commands/list_builds.py b/cli/koji_cli/commands/list_builds.py new file mode 100644 index 0000000..ffdb53f --- /dev/null +++ b/cli/koji_cli/commands/list_builds.py @@ -0,0 +1,142 @@ +from __future__ import absolute_import, division + +from optparse import OptionParser + +import koji + +from koji_cli.lib import ( + TimeOption, + ensure_connection, + get_usage_str, + warn +) + + +def anon_handle_list_builds(goptions, session, args): + "[info] Print the build listing" + usage = "usage: %prog list-builds [options]" + parser = OptionParser(usage=get_usage_str(usage), option_class=TimeOption) + parser.add_option("--package", help="List builds for this package") + parser.add_option("--buildid", help="List specific build from ID or nvr") + parser.add_option("--before", type="time", + help="List builds built before this time, " + TimeOption.get_help()) + parser.add_option("--after", type="time", + help="List builds built after this time (same format as for --before") + parser.add_option("--state", help="List builds in this state") + parser.add_option("--task", help="List builds for this task") + parser.add_option("--type", help="List builds of this type.") + parser.add_option("--prefix", help="Only builds starting with this prefix") + parser.add_option("--pattern", help="Only list builds matching this GLOB pattern") + parser.add_option("--cg", help="Only list builds imported by matching content generator name") + parser.add_option("--source", help="Only builds where the source field matches (glob pattern)") + parser.add_option("--owner", help="List builds built by this owner") + parser.add_option("--volume", help="List builds by volume ID") + parser.add_option("-k", "--sort-key", action="append", metavar='FIELD', + default=[], help="Sort the list by the named field. Allowed sort keys: " + "nvr, owner_name, state") + parser.add_option("-r", "--reverse", action="store_true", default=False, + help="Print the list in reverse order") + parser.add_option("--quiet", action="store_true", default=goptions.quiet, + help="Do not print the header information") + (options, args) = parser.parse_args(args) + if len(args) != 0: + parser.error("This command takes no arguments") + ensure_connection(session, goptions) + opts = {} + for key in ('type', 'prefix', 'pattern'): + value = getattr(options, key) + if value is not None: + opts[key] = value + if options.cg: + opts['cgID'] = options.cg + if options.package: + try: + opts['packageID'] = int(options.package) + except ValueError: + package = session.getPackageID(options.package) + if package is None: + parser.error("No such package: %s" % options.package) + opts['packageID'] = package + if options.owner: + try: + opts['userID'] = int(options.owner) + except ValueError: + user = session.getUser(options.owner) + if user is None: + parser.error("No such user: %s" % options.owner) + opts['userID'] = user['id'] + if options.volume: + try: + opts['volumeID'] = int(options.volume) + except ValueError: + volumes = session.listVolumes() + volumeID = None + for volume in volumes: + if options.volume == volume['name']: + volumeID = volume['id'] + if volumeID is None: + parser.error("No such volume: %s" % options.volume) + opts['volumeID'] = volumeID + if options.state: + try: + state = int(options.state) + if state > 4 or state < 0: + parser.error("Invalid state: %s" % options.state) + opts['state'] = state + except ValueError: + try: + opts['state'] = koji.BUILD_STATES[options.state] + except KeyError: + parser.error("Invalid state: %s" % options.state) + if options.before: + opts['completeBefore'] = options.before + if options.after: + opts['completeAfter'] = options.after + if options.task: + try: + opts['taskID'] = int(options.task) + except ValueError: + parser.error("Task id must be an integer") + if options.source: + opts['source'] = options.source + if options.buildid: + try: + buildid = int(options.buildid) + except ValueError: + buildid = options.buildid + data = [session.getBuild(buildid)] + if data[0] is None: + parser.error("No such build: '%s'" % buildid) + else: + # Check filter exists + if any(opts): + try: + data = session.listBuilds(**opts) + except koji.ParameterError as e: + if e.args[0].endswith("'pattern'"): + parser.error("The hub doesn't support the 'pattern' argument, please try" + " filtering the result on your local instead.") + if e.args[0].endswith("'cgID'"): + parser.error("The hub doesn't support the 'cg' argument, please try" + " filtering the result on your local instead.") + else: + parser.error("Filter must be provided for list") + if not options.sort_key: + options.sort_key = ['nvr'] + else: + for s_key in options.sort_key: + if s_key not in ['nvr', 'owner_name', 'state']: + warn("Invalid sort_key: %s." % s_key) + + data = sorted(data, key=lambda b: [b.get(k) for k in options.sort_key], + reverse=options.reverse) + for build in data: + build['state'] = koji.BUILD_STATES[build['state']] + + fmt = "%(nvr)-55s %(owner_name)-16s %(state)s" + if not options.quiet: + print("%-55s %-16s %s" % ("Build", "Built by", "State")) + print("%s %s %s" % ("-" * 55, "-" * 16, "-" * 16)) + + for build in data: + print(fmt % build) diff --git a/cli/koji_cli/commands/list_channels.py b/cli/koji_cli/commands/list_channels.py new file mode 100644 index 0000000..bec0551 --- /dev/null +++ b/cli/koji_cli/commands/list_channels.py @@ -0,0 +1,93 @@ +from __future__ import absolute_import, division + +from optparse import OptionParser + +from six.moves import zip + +import koji + +from koji_cli.lib import ( + ensure_connection, + get_usage_str, + truncate_string +) + + +def anon_handle_list_channels(goptions, session, args): + "[info] Print channels listing" + usage = "usage: %prog list-channels [options]" + parser = OptionParser(usage=get_usage_str(usage)) + parser.add_option("--simple", action="store_true", default=False, + help="Print just list of channels without additional info") + parser.add_option("--quiet", action="store_true", default=goptions.quiet, + help="Do not print header information") + parser.add_option("--comment", action="store_true", help="Show comments") + parser.add_option("--description", action="store_true", help="Show descriptions") + parser.add_option("--enabled", action="store_true", help="Limit to enabled channels") + parser.add_option("--not-enabled", action="store_false", dest="enabled", + help="Limit to not enabled channels") + parser.add_option("--disabled", action="store_false", dest="enabled", + help="Alias for --not-enabled") + (options, args) = parser.parse_args(args) + ensure_connection(session, goptions) + opts = {} + if options.enabled is not None: + opts['enabled'] = options.enabled + try: + channels = sorted([x for x in session.listChannels(**opts)], key=lambda x: x['name']) + except koji.ParameterError: + channels = sorted([x for x in session.listChannels()], key=lambda x: x['name']) + if len(channels) > 0: + first_item = channels[0] + else: + first_item = {} + session.multicall = True + for channel in channels: + session.listHosts(channelID=channel['id']) + for channel, [hosts] in zip(channels, session.multiCall()): + channel['enabled_host'] = len([x for x in hosts if x['enabled']]) + channel['disabled'] = len(hosts) - channel['enabled_host'] + channel['ready'] = len([x for x in hosts if x['ready']]) + channel['capacity'] = sum([x['capacity'] for x in hosts]) + channel['load'] = sum([x['task_load'] for x in hosts]) + if 'comment' in first_item: + channel['comment'] = truncate_string(channel['comment']) + if 'description' in first_item: + channel['description'] = truncate_string(channel['description']) + if channel['capacity']: + channel['perc_load'] = channel['load'] / channel['capacity'] * 100.0 + else: + channel['perc_load'] = 0.0 + if 'enabled' in first_item: + if not channel['enabled']: + channel['name'] = channel['name'] + ' [disabled]' + if channels: + longest_channel = max([len(ch['name']) for ch in channels]) + else: + longest_channel = 8 + if options.simple: + if not options.quiet: + hdr = 'Channel' + print(hdr) + print(len(hdr) * '-') + for channel in channels: + print(channel['name']) + else: + if not options.quiet: + hdr = '{channame:<{longest_channel}}Enabled Ready Disbld Load Cap ' \ + 'Perc ' + hdr = hdr.format(longest_channel=longest_channel, channame='Channel') + if options.description and 'description' in first_item: + hdr += "Description".ljust(53) + if options.comment and 'comment' in first_item: + hdr += "Comment".ljust(53) + print(hdr) + print(len(hdr) * '-') + mask = "%%(name)-%ss %%(enabled_host)6d %%(ready)6d %%(disabled)6d %%(load)6d %%(" \ + "capacity)6d %%(perc_load)6d%%%%" % longest_channel + if options.description and 'description' in first_item: + mask += " %(description)-50s" + if options.comment and 'comment' in first_item: + mask += " %(comment)-50s" + for channel in channels: + print(mask % channel) diff --git a/cli/koji_cli/commands/list_external_repos.py b/cli/koji_cli/commands/list_external_repos.py new file mode 100644 index 0000000..f3620ff --- /dev/null +++ b/cli/koji_cli/commands/list_external_repos.py @@ -0,0 +1,87 @@ +from __future__ import absolute_import, division + +import time +from optparse import OptionParser + +import koji + +from koji_cli.lib import ( + ensure_connection, + get_usage_str +) + + +def anon_handle_list_external_repos(goptions, session, args): + "[info] List external repos" + usage = "usage: %prog list-external-repos [options]" + parser = OptionParser(usage=get_usage_str(usage)) + parser.add_option("--url", help="Select by url") + parser.add_option("--name", help="Select by name") + parser.add_option("--id", type="int", help="Select by id") + parser.add_option("--tag", help="Select by tag") + parser.add_option("--used", action='store_true', help="List which tags use the repo(s)") + parser.add_option("--inherit", action='store_true', + help="Follow tag inheritance when selecting by tag") + parser.add_option("--event", type='int', metavar="EVENT#", help="Query at event") + parser.add_option("--ts", type='int', metavar="TIMESTAMP", + help="Query at last event before timestamp") + parser.add_option("--repo", type='int', metavar="REPO#", + help="Query at event corresponding to (nonexternal) repo") + parser.add_option("--quiet", action="store_true", default=goptions.quiet, + help="Do not display the column headers") + (options, args) = parser.parse_args(args) + if len(args) > 0: + parser.error("This command takes no arguments") + ensure_connection(session, goptions) + opts = {} + event = koji.util.eventFromOpts(session, options) + if event: + opts['event'] = event['id'] + event['timestr'] = time.asctime(time.localtime(event['ts'])) + print("Querying at event %(id)i (%(timestr)s)" % event) + if options.tag: + format = "tag" + opts['tag_info'] = options.tag + opts['repo_info'] = options.id or options.name or None + if opts['repo_info']: + if options.inherit: + parser.error("Can't select by repo when using --inherit") + if options.inherit: + del opts['repo_info'] + data = session.getExternalRepoList(**opts) + format = "multitag" + else: + data = session.getTagExternalRepos(**opts) + elif options.used: + format = "multitag" + opts['repo_info'] = options.id or options.name or None + data = session.getTagExternalRepos(**opts) + else: + format = "basic" + opts['info'] = options.id or options.name or None + opts['url'] = options.url or None + data = session.listExternalRepos(**opts) + + # There are three different output formats + # 1) Listing just repo data (name, url) + # 2) Listing repo data for a tag (priority, name, url) + # 3) Listing repo data for multiple tags (tag, priority, name, url) + if format == "basic": + format = "%(name)-25s %(url)s" + header1 = "%-25s %s" % ("External repo name", "URL") + header2 = "%s %s" % ("-" * 25, "-" * 40) + elif format == "tag": + format = "%(priority)-3i %(external_repo_name)-25s %(merge_mode)-10s %(url)s" + header1 = "%-3s %-25s %-10s URL" % ("Pri", "External repo name", "Mode") + header2 = "%s %s %s %s" % ("-" * 3, "-" * 25, "-" * 10, "-" * 40) + elif format == "multitag": + format = "%(tag_name)-20s %(priority)-3i %(merge_mode)-10s %(external_repo_name)s" + header1 = "%-20s %-3s %-10s %s" % ("Tag", "Pri", "Mode", "External repo name") + header2 = "%s %s %s %s" % ("-" * 20, "-" * 3, "-" * 10, "-" * 25) + if not options.quiet: + print(header1) + print(header2) + for rinfo in data: + # older hubs do not support merge_mode + rinfo.setdefault('merge_mode', None) + print(format % rinfo) diff --git a/cli/koji_cli/commands/list_groups.py b/cli/koji_cli/commands/list_groups.py new file mode 100644 index 0000000..fe29b13 --- /dev/null +++ b/cli/koji_cli/commands/list_groups.py @@ -0,0 +1,72 @@ +from __future__ import absolute_import, division + +import time +from optparse import OptionParser + +import koji + +from koji_cli.lib import ( + ensure_connection, + get_usage_str +) + + +def print_group_list_req_group(group): + print(" @%(name)s [%(tag_name)s]" % group) + + +def print_group_list_req_package(pkg): + fmt = " %(package)s: %(basearchonly)s, %(type)s [%(tag_name)s]" + if pkg['blocked']: + fmt += " [BLOCKED]" + print(fmt % pkg) + + +def anon_handle_list_groups(goptions, session, args): + "[info] Print the group listings" + usage = "usage: %prog list-groups [options] []" + parser = OptionParser(usage=get_usage_str(usage)) + parser.add_option("--event", type='int', metavar="EVENT#", help="query at event") + parser.add_option("--ts", type='int', metavar="TIMESTAMP", + help="query at last event before timestamp") + parser.add_option("--repo", type='int', metavar="REPO#", help="query at event for a repo") + parser.add_option("--show-blocked", action="store_true", dest="incl_blocked", + help="Show blocked packages and groups") + (options, args) = parser.parse_args(args) + if len(args) < 1 or len(args) > 2: + parser.error("Incorrect number of arguments") + opts = {} + if options.incl_blocked: + opts['incl_blocked'] = True + ensure_connection(session, goptions) + event = koji.util.eventFromOpts(session, options) + if event: + opts['event'] = event['id'] + event['timestr'] = time.asctime(time.localtime(event['ts'])) + print("Querying at event %(id)i (%(timestr)s)" % event) + tmp_list = sorted([(x['name'], x) for x in session.getTagGroups(args[0], **opts)]) + groups = [x[1] for x in tmp_list] + + tags_cache = {} + + def get_cached_tag(tag_id): + if tag_id not in tags_cache: + tag = session.getTag(tag_id, strict=False) + if tag is None: + tags_cache[tag_id] = tag_id + else: + tags_cache[tag_id] = tag['name'] + return tags_cache[tag_id] + + for group in groups: + if len(args) > 1 and group['name'] != args[1]: + continue + print("%s [%s]" % (group['name'], get_cached_tag(group['tag_id']))) + groups = sorted([(x['name'], x) for x in group['grouplist']]) + for x in [x[1] for x in groups]: + x['tag_name'] = get_cached_tag(x['tag_id']) + print_group_list_req_group(x) + pkgs = sorted([(x['package'], x) for x in group['packagelist']]) + for x in [x[1] for x in pkgs]: + x['tag_name'] = get_cached_tag(x['tag_id']) + print_group_list_req_package(x) diff --git a/cli/koji_cli/commands/list_history.py b/cli/koji_cli/commands/list_history.py new file mode 100644 index 0000000..79601f1 --- /dev/null +++ b/cli/koji_cli/commands/list_history.py @@ -0,0 +1,574 @@ +from __future__ import absolute_import, division + +import logging +import pprint +import sys +import time +import traceback +from datetime import datetime +from dateutil.tz import tzutc +from optparse import SUPPRESS_HELP, OptionParser + +from six.moves import zip + +import koji + +from koji.util import to_list +from koji_cli.lib import ( + TimeOption, + ensure_connection, + error, + get_usage_str, + warn, +) + + +def _print_histline(entry, **kwargs): + options = kwargs['options'] + event_id, table, create, x = entry + who = None + edit = x.get('.related') + if edit: + del x['.related'] + bad_edit = None + if len(edit) != 1: + bad_edit = "%i elements" % (len(edit) + 1) + other = edit[0] + # check edit for sanity + if create or not other[2]: + bad_edit = "out of order" + if event_id != other[0]: + bad_edit = "non-matching" + if bad_edit: + warn("Unusual edit at event %i in table %s (%s)" % (event_id, table, bad_edit)) + # we'll simply treat them as separate events + pprint.pprint(entry) + pprint.pprint(edit) + _print_histline(entry, **kwargs) + for data in edit: + _print_histline(entry, **kwargs) + return + if create: + ts = x['create_ts'] + if 'creator_name' in x: + who = "by %(creator_name)s" + else: + ts = x['revoke_ts'] + if 'revoker_name' in x: + who = "by %(revoker_name)s" + if table == 'tag_listing': + if edit: + fmt = "%(name)s-%(version)s-%(release)s re-tagged into %(tag.name)s" + elif create: + fmt = "%(name)s-%(version)s-%(release)s tagged into %(tag.name)s" + else: + fmt = "%(name)s-%(version)s-%(release)s untagged from %(tag.name)s" + elif table == 'user_perms': + if edit: + fmt = "permission %(permission.name)s re-granted to %(user.name)s" + elif create: + fmt = "permission %(permission.name)s granted to %(user.name)s" + else: + fmt = "permission %(permission.name)s revoked for %(user.name)s" + elif table == 'user_groups': + if edit: + fmt = "user %(user.name)s re-added to group %(group.name)s" + elif create: + fmt = "user %(user.name)s added to group %(group.name)s" + else: + fmt = "user %(user.name)s removed from group %(group.name)s" + elif table == 'cg_users': + if edit: + fmt = "user %(user.name)s re-added to content generator %(content_generator.name)s" + elif create: + fmt = "user %(user.name)s added to content generator %(content_generator.name)s" + else: + fmt = "user %(user.name)s removed from content generator %(content_generator.name)s" + elif table == 'tag_packages': + if edit: + fmt = "package list entry for %(package.name)s in %(tag.name)s updated" + elif create: + fmt = "package list entry created: %(package.name)s in %(tag.name)s" + else: + fmt = "package list entry revoked: %(package.name)s in %(tag.name)s" + elif table == 'tag_package_owners': + if edit: + fmt = "package owner changed for %(package.name)s in %(tag.name)s" + elif create: + fmt = "package owner %(owner.name)s set for %(package.name)s in %(tag.name)s" + else: + fmt = "package owner %(owner.name)s revoked for %(package.name)s in %(tag.name)s" + elif table == 'tag_inheritance': + if edit: + fmt = "inheritance line %(tag.name)s->%(parent.name)s updated" + elif create: + fmt = "inheritance line %(tag.name)s->%(parent.name)s added" + else: + fmt = "inheritance line %(tag.name)s->%(parent.name)s removed" + elif table == 'tag_config': + if edit: + fmt = "tag configuration for %(tag.name)s altered" + elif create: + fmt = "new tag: %(tag.name)s" + else: + fmt = "tag deleted: %(tag.name)s" + elif table == 'tag_extra': + if edit: + fmt = "tag option %(key)s for tag %(tag.name)s altered" + elif create: + fmt = "added tag option %(key)s for tag %(tag.name)s" + else: + fmt = "tag option %(key)s removed for %(tag.name)s" + elif table == 'host_config': + if edit: + fmt = "host configuration for %(host.name)s altered" + elif create: + fmt = "new host: %(host.name)s" + else: + fmt = "host deleted: %(host.name)s" + elif table == 'host_channels': + if create: + fmt = "host %(host.name)s added to channel %(channels.name)s" + else: + fmt = "host %(host.name)s removed from channel %(channels.name)s" + elif table == 'build_target_config': + if edit: + fmt = "build target configuration for %(build_target.name)s updated" + elif create: + fmt = "new build target: %(build_target.name)s" + else: + fmt = "build target deleted: %(build_target.name)s" + elif table == 'external_repo_config': + if edit: + fmt = "external repo configuration for %(external_repo.name)s altered" + elif create: + fmt = "new external repo: %(external_repo.name)s" + else: + fmt = "external repo deleted: %(external_repo.name)s" + elif table == 'tag_external_repos': + if edit: + fmt = "external repo entry for %(external_repo.name)s in tag %(tag.name)s updated" + elif create: + fmt = "external repo entry for %(external_repo.name)s added to tag %(tag.name)s" + else: + fmt = "external repo entry for %(external_repo.name)s removed from tag %(tag.name)s" + elif table == 'group_config': + if edit: + fmt = "group %(group.name)s configuration for tag %(tag.name)s updated" + elif create: + fmt = "group %(group.name)s added to tag %(tag.name)s" + else: + fmt = "group %(group.name)s removed from tag %(tag.name)s" + elif table == 'group_req_listing': + if edit: + fmt = "group dependency %(group.name)s->%(req.name)s updated in tag %(tag.name)s" + elif create: + fmt = "group dependency %(group.name)s->%(req.name)s added in tag %(tag.name)s" + else: + fmt = "group dependency %(group.name)s->%(req.name)s dropped from tag %(tag.name)s" + elif table == 'group_package_listing': + if edit: + fmt = "package entry %(package)s in group %(group.name)s, tag %(tag.name)s updated" + elif create: + fmt = "package %(package)s added to group %(group.name)s in tag %(tag.name)s" + else: + fmt = "package %(package)s removed from group %(group.name)s in tag %(tag.name)s" + else: + if edit: + fmt = "%s entry updated" % table + elif create: + fmt = "%s entry created" % table + else: + fmt = "%s entry revoked" % table + if options.utc: + time_str = time.asctime(datetime.fromtimestamp(ts, tzutc()).timetuple()) + else: + time_str = time.asctime(time.localtime(ts)) + + parts = [time_str, fmt % x] + if options.events or options.verbose: + parts.insert(1, "(eid %i)" % event_id) + if who: + parts.append(who % x) + if create and x['active']: + parts.append("[still active]") + print(' '.join(parts)) + hidden_fields = ['active', 'create_event', 'revoke_event', 'creator_id', 'revoker_id', + 'creator_name', 'revoker_name', 'create_ts', 'revoke_ts'] + + def get_nkey(key): + if key == 'perm_id': + return 'permission.name' + elif key.endswith('_id'): + return '%s.name' % key[:-3] + else: + return '%s.name' % key + if edit: + keys = sorted(to_list(x.keys())) + y = other[-1] + for key in keys: + if key in hidden_fields: + continue + if x[key] == y[key]: + continue + if key[0] == '_': + continue + nkey = get_nkey(key) + if nkey in x and nkey in y: + continue + print(" %s: %s -> %s" % (key, x[key], y[key])) + elif create and options.verbose and table != 'tag_listing': + keys = sorted(to_list(x.keys())) + # the table keys have already been represented in the base format string + also_hidden = list(_table_keys[table]) + also_hidden.extend([get_nkey(k) for k in also_hidden]) + for key in keys: + if key in hidden_fields or key in also_hidden: + continue + nkey = get_nkey(key) + if nkey in x: + continue + if key[0] == '_': + continue + if x.get('blocked') and key != 'blocked': + continue + if key.endswith('.name'): + dkey = key[:-5] + else: + dkey = key + print(" %s: %s" % (dkey, x[key])) + + +_table_keys = { + 'user_perms': ['user_id', 'perm_id'], + 'user_groups': ['user_id', 'group_id'], + 'cg_users': ['user_id', 'cg_id'], + 'tag_inheritance': ['tag_id', 'parent_id'], + 'tag_config': ['tag_id'], + 'tag_extra': ['tag_id', 'key'], + 'build_target_config': ['build_target_id'], + 'external_repo_config': ['external_repo_id'], + 'host_config': ['host_id'], + 'host_channels': ['host_id', 'channel_id'], + 'tag_external_repos': ['tag_id', 'external_repo_id'], + 'tag_listing': ['build_id', 'tag_id'], + 'tag_packages': ['package_id', 'tag_id'], + 'tag_package_owners': ['package_id', 'tag_id'], + 'group_config': ['group_id', 'tag_id'], + 'group_req_listing': ['group_id', 'tag_id', 'req_id'], + 'group_package_listing': ['group_id', 'tag_id', 'package'], +} + + +def anon_handle_list_history(goptions, session, args): + "[info] Display historical data" + usage = "usage: %prog list-history [options]" + parser = OptionParser(usage=get_usage_str(usage), option_class=TimeOption) + # Don't use local debug option, this one stays here for backward compatibility + # https://pagure.io/koji/issue/2084 + parser.add_option("--debug", action="store_true", default=goptions.debug, help=SUPPRESS_HELP) + parser.add_option("--build", help="Only show data for a specific build") + parser.add_option("--package", help="Only show data for a specific package") + parser.add_option("--tag", help="Only show data for a specific tag") + parser.add_option("--editor", "--by", metavar="USER", + help="Only show entries modified by user") + parser.add_option("--user", help="Only show entries affecting a user") + parser.add_option("--permission", help="Only show entries relating to a given permission") + parser.add_option("--cg", help="Only show entries relating to a given content generator") + parser.add_option("--external-repo", "--erepo", + help="Only show entries relating to a given external repo") + parser.add_option("--build-target", "--target", + help="Only show entries relating to a given build target") + parser.add_option("--group", help="Only show entries relating to a given group") + parser.add_option("--host", help="Only show entries related to given host") + parser.add_option("--channel", help="Only show entries related to given channel") + parser.add_option("--xkey", help="Only show entries related to given tag extra key") + parser.add_option("--before", type="time", + help="Only show entries before this time, " + TimeOption.get_help()) + parser.add_option("--after", type="time", + help="Only show entries after timestamp (same format as for --before)") + parser.add_option("--before-event", metavar="EVENT_ID", type='int', + help="Only show entries before event") + parser.add_option("--after-event", metavar="EVENT_ID", type='int', + help="Only show entries after event") + parser.add_option("--watch", action="store_true", help="Monitor history data") + parser.add_option("--active", action='store_true', + help="Only show entries that are currently active") + parser.add_option("--revoked", action='store_false', dest='active', + help="Only show entries that are currently revoked") + parser.add_option("--context", action="store_true", help="Show related entries") + parser.add_option("-s", "--show", action="append", help="Show data from selected tables") + parser.add_option("-v", "--verbose", action="store_true", help="Show more detail") + parser.add_option("-e", "--events", action="store_true", help="Show event ids") + parser.add_option("--all", action="store_true", + help="Allows listing the entire global history") + parser.add_option("--utc", action="store_true", + help="Shows time in UTC timezone") + (options, args) = parser.parse_args(args) + if len(args) != 0: + parser.error("This command takes no arguments") + kwargs = {} + limited = False + for opt in ('package', 'tag', 'build', 'editor', 'user', 'permission', + 'cg', 'external_repo', 'build_target', 'group', 'before', + 'after', 'host', 'channel', 'xkey'): + val = getattr(options, opt) + if val: + kwargs[opt] = val + limited = True + if options.before_event: + kwargs['beforeEvent'] = options.before_event + if options.after_event: + kwargs['afterEvent'] = options.after_event + if options.active is not None: + kwargs['active'] = options.active + if options.host: + hostinfo = session.getHost(options.host) + if not hostinfo: + error("No such host: %s" % options.host) + if options.channel: + channelinfo = session.getChannel(options.channel) + if not channelinfo: + error("No such channel: %s" % options.channel) + if options.utc: + kwargs['utc'] = options.utc + tables = None + if options.show: + tables = [] + for arg in options.show: + tables.extend(arg.split(',')) + if not limited and not options.all: + parser.error("Please specify an option to limit the query") + + ensure_connection(session, goptions) + + if options.watch: + if not kwargs.get('afterEvent') and not kwargs.get('after'): + kwargs['afterEvent'] = session.getLastEvent()['id'] + + while True: + histdata = session.queryHistory(tables=tables, **kwargs) + timeline = [] + + def distinguish_match(x, name): + """determine if create or revoke event matched""" + if options.context: + return True + name = "_" + name + ret = True + for key in x: + if key.startswith(name): + ret = ret and x[key] + return ret + for table in histdata: + hist = histdata[table] + for x in hist: + if x['revoke_event'] is not None: + if distinguish_match(x, 'revoked'): + timeline.append((x['revoke_event'], table, 0, x.copy())) + # pprint.pprint(timeline[-1]) + if distinguish_match(x, 'created'): + timeline.append((x['create_event'], table, 1, x)) + timeline.sort(key=lambda entry: entry[:3]) + # group edits together + new_timeline = [] + last_event = None + edit_index = {} + for entry in timeline: + event_id, table, create, x = entry + if event_id != last_event: + edit_index = {} + last_event = event_id + key = tuple([x[k] for k in _table_keys[table]]) + prev = edit_index.get((table, event_id), {}).get(key) + if prev: + prev[-1].setdefault('.related', []).append(entry) + else: + edit_index.setdefault((table, event_id), {})[key] = entry + new_timeline.append(entry) + for entry in new_timeline: + if options.debug: + print("%r" % list(entry)) + _print_histline(entry, options=options) + if not options.watch: + break + else: + time.sleep(goptions.poll_interval) + # repeat query for later events + if last_event: + kwargs['afterEvent'] = last_event + + +def _handleMap(lines, data, prefix=''): + for key, val in data.items(): + if key != '__starstar': + lines.append(' %s%s: %s' % (prefix, key, val)) + + +def _handleOpts(lines, opts, prefix=''): + if opts: + lines.append("%sOptions:" % prefix) + _handleMap(lines, opts, prefix) + + +def _parseTaskParams(session, method, task_id, topdir): + try: + return _do_parseTaskParams(session, method, task_id, topdir) + except Exception: + logger = logging.getLogger("koji") + if logger.isEnabledFor(logging.DEBUG): + tb_str = ''.join(traceback.format_exception(*sys.exc_info())) + logger.debug(tb_str) + return ['Unable to parse task parameters'] + + +def _do_parseTaskParams(session, method, task_id, topdir): + """Parse the return of getTaskRequest()""" + params = session.getTaskRequest(task_id) + + lines = [] + + if method == 'buildSRPMFromCVS': + lines.append("CVS URL: %s" % params[0]) + elif method == 'buildSRPMFromSCM': + lines.append("SCM URL: %s" % params[0]) + elif method == 'buildArch': + lines.append("SRPM: %s/work/%s" % (topdir, params[0])) + lines.append("Build Tag: %s" % session.getTag(params[1])['name']) + lines.append("Build Arch: %s" % params[2]) + lines.append("SRPM Kept: %r" % params[3]) + if len(params) > 4: + _handleOpts(lines, params[4]) + elif method == 'tagBuild': + build = session.getBuild(params[1]) + lines.append("Destination Tag: %s" % session.getTag(params[0])['name']) + lines.append("Build: %s" % koji.buildLabel(build)) + elif method == 'buildNotification': + build = params[1] + buildTarget = params[2] + lines.append("Recipients: %s" % (", ".join(params[0]))) + lines.append("Build: %s" % koji.buildLabel(build)) + lines.append("Build Target: %s" % buildTarget['name']) + lines.append("Web URL: %s" % params[3]) + elif method == 'build': + lines.append("Source: %s" % params[0]) + lines.append("Build Target: %s" % params[1]) + if len(params) > 2: + _handleOpts(lines, params[2]) + elif method == 'maven': + lines.append("SCM URL: %s" % params[0]) + lines.append("Build Target: %s" % params[1]) + if len(params) > 2: + _handleOpts(lines, params[2]) + elif method == 'buildMaven': + lines.append("SCM URL: %s" % params[0]) + lines.append("Build Tag: %s" % params[1]['name']) + if len(params) > 2: + _handleOpts(lines, params[2]) + elif method == 'wrapperRPM': + lines.append("Spec File URL: %s" % params[0]) + lines.append("Build Tag: %s" % params[1]['name']) + if params[2]: + lines.append("Build: %s" % koji.buildLabel(params[2])) + if params[3]: + lines.append("Task: %s %s" % (params[3]['id'], koji.taskLabel(params[3]))) + if len(params) > 4: + _handleOpts(lines, params[4]) + elif method == 'chainmaven': + lines.append("Builds:") + for package, opts in params[0].items(): + lines.append(" " + package) + _handleMap(lines, opts, prefix=" ") + lines.append("Build Target: %s" % params[1]) + if len(params) > 2: + _handleOpts(lines, params[2]) + elif method == 'winbuild': + lines.append("VM: %s" % params[0]) + lines.append("SCM URL: %s" % params[1]) + lines.append("Build Target: %s" % params[2]) + if len(params) > 3: + _handleOpts(lines, params[3]) + elif method == 'vmExec': + lines.append("VM: %s" % params[0]) + lines.append("Exec Params:") + for info in params[1]: + if isinstance(info, dict): + _handleMap(lines, info, prefix=' ') + else: + lines.append(" %s" % info) + if len(params) > 2: + _handleOpts(lines, params[2]) + elif method in ('createLiveCD', 'createAppliance', 'createLiveMedia'): + argnames = ['Name', 'Version', 'Release', 'Arch', 'Target Info', 'Build Tag', 'Repo', + 'Kickstart File'] + for n, v in zip(argnames, params): + lines.append("%s: %s" % (n, v)) + if len(params) > 8: + _handleOpts(lines, params[8]) + elif method in ('appliance', 'livecd', 'livemedia'): + argnames = ['Name', 'Version', 'Arches', 'Target', 'Kickstart'] + for n, v in zip(argnames, params): + lines.append("%s: %s" % (n, v)) + if len(params) > 5: + _handleOpts(lines, params[5]) + elif method == 'newRepo': + tag = session.getTag(params[0]) + lines.append("Tag: %s" % tag['name']) + elif method == 'prepRepo': + lines.append("Tag: %s" % params[0]['name']) + elif method == 'createrepo': + lines.append("Repo ID: %i" % params[0]) + lines.append("Arch: %s" % params[1]) + oldrepo = params[2] + if oldrepo: + lines.append("Old Repo ID: %i" % oldrepo['id']) + lines.append("Old Repo Creation: %s" % koji.formatTimeLong(oldrepo['create_ts'])) + if len(params) > 3: + lines.append("External Repos: %s" % + ', '.join([ext['external_repo_name'] for ext in params[3]])) + elif method == 'tagNotification': + destTag = session.getTag(params[2]) + srcTag = None + if params[3]: + srcTag = session.getTag(params[3]) + build = session.getBuild(params[4]) + user = session.getUser(params[5]) + + lines.append("Recipients: %s" % ", ".join(params[0])) + lines.append("Successful?: %s" % (params[1] and 'yes' or 'no')) + lines.append("Tagged Into: %s" % destTag['name']) + if srcTag: + lines.append("Moved From: %s" % srcTag['name']) + lines.append("Build: %s" % koji.buildLabel(build)) + lines.append("Tagged By: %s" % user['name']) + lines.append("Ignore Success?: %s" % (params[6] and 'yes' or 'no')) + if params[7]: + lines.append("Failure Message: %s" % params[7]) + elif method == 'dependantTask': + lines.append("Dependant Tasks: %s" % ", ".join([str(depID) for depID in params[0]])) + lines.append("Subtasks:") + for subtask in params[1]: + lines.append(" Method: %s" % subtask[0]) + lines.append(" Parameters: %s" % + ", ".join([str(subparam) for subparam in subtask[1]])) + if len(subtask) > 2 and subtask[2]: + subopts = subtask[2] + _handleOpts(lines, subopts, prefix=' ') + lines.append("") + elif method == 'chainbuild': + lines.append("Build Groups:") + group_num = 0 + for group_list in params[0]: + group_num += 1 + lines.append(" %i: %s" % (group_num, ', '.join(group_list))) + lines.append("Build Target: %s" % params[1]) + if len(params) > 2: + _handleOpts(lines, params[2]) + elif method == 'waitrepo': + lines.append("Build Target: %s" % params[0]) + if params[1]: + lines.append("Newer Than: %s" % params[1]) + if params[2]: + lines.append("NVRs: %s" % ', '.join(params[2])) + + return lines diff --git a/cli/koji_cli/commands/list_hosts.py b/cli/koji_cli/commands/list_hosts.py new file mode 100644 index 0000000..9099476 --- /dev/null +++ b/cli/koji_cli/commands/list_hosts.py @@ -0,0 +1,130 @@ +from __future__ import absolute_import, division + +from optparse import OptionParser + +from six.moves import zip + +import koji + +from koji_cli.lib import ( + ensure_connection, + get_usage_str, + warn, + truncate_string +) + + +def anon_handle_list_hosts(goptions, session, args): + "[info] Print the host listing" + usage = "usage: %prog list-hosts [options]" + parser = OptionParser(usage=get_usage_str(usage)) + parser.add_option("--arch", action="append", default=[], help="Specify an architecture") + parser.add_option("--channel", help="Specify a channel") + parser.add_option("--ready", action="store_true", help="Limit to ready hosts") + parser.add_option("--not-ready", action="store_false", dest="ready", + help="Limit to not ready hosts") + parser.add_option("--enabled", action="store_true", help="Limit to enabled hosts") + parser.add_option("--not-enabled", action="store_false", dest="enabled", + help="Limit to not enabled hosts") + parser.add_option("--disabled", action="store_false", dest="enabled", + help="Alias for --not-enabled") + parser.add_option("--quiet", action="store_true", default=goptions.quiet, + help="Do not print header information") + parser.add_option("--show-channels", action="store_true", help="Show host's channels") + parser.add_option("--comment", action="store_true", help="Show comments") + parser.add_option("--description", action="store_true", help="Show descriptions") + (options, args) = parser.parse_args(args) + opts = {} + ensure_connection(session, goptions) + if options.arch: + opts['arches'] = options.arch + if options.channel: + channel = session.getChannel(options.channel) + if not channel: + parser.error('No such channel: %s' % options.channel) + opts['channelID'] = channel['id'] + if options.ready is not None: + opts['ready'] = options.ready + if options.enabled is not None: + opts['enabled'] = options.enabled + tmp_list = sorted([(x['name'], x) for x in session.listHosts(**opts)]) + hosts = [x[1] for x in tmp_list] + + if not hosts: + warn("No hosts found.") + return + + def yesno(x): + if x: + return 'Y' + else: + return 'N' + + try: + first = session.getLastHostUpdate(hosts[0]['id'], ts=True) + opts = {'ts': True} + except koji.ParameterError: + # Hubs prior to v1.25.0 do not have a "ts" parameter for getLastHostUpdate + first = session.getLastHostUpdate(hosts[0]['id']) + opts = {} + + # pull in the last update using multicall to speed it up a bit + with session.multicall() as m: + result = [m.getLastHostUpdate(host['id'], **opts) for host in hosts[1:]] + updateList = [first] + [x.result for x in result] + + for host, update in zip(hosts, updateList): + if update is None: + host['update'] = '-' + else: + host['update'] = koji.formatTimeLong(update) + host['enabled'] = yesno(host['enabled']) + host['ready'] = yesno(host['ready']) + host['arches'] = ','.join(host['arches'].split()) + host['description'] = truncate_string(host['description']) + host['comment'] = truncate_string(host['comment']) + + # pull hosts' channels + if options.show_channels: + with session.multicall() as m: + result = [m.listChannels(host['id']) for host in hosts] + first_line_channel = result[0].result[0] + for host, channels in zip(hosts, result): + list_channels = [] + for c in channels.result: + if 'enabled' in first_line_channel: + if c['enabled']: + list_channels.append(c['name']) + else: + list_channels.append('*' + c['name']) + else: + list_channels.append(c['name']) + host['channels'] = ','.join(sorted(list_channels)) + + if hosts: + longest_host = max([len(h['name']) for h in hosts]) + else: + longest_host = 8 + + if not options.quiet: + hdr = "{hostname:<{longest_host}} Enb Rdy Load/Cap Arches " \ + "Last Update " + hdr = hdr.format(longest_host=longest_host, hostname='Hostname') + if options.description: + hdr += "Description".ljust(51) + if options.comment: + hdr += "Comment".ljust(51) + if options.show_channels: + hdr += "Channels" + print(hdr) + print(len(hdr) * '-') + mask = "%%(name)-%ss %%(enabled)-3s %%(ready)-3s %%(task_load)4.1f/%%(capacity)-4.1f " \ + "%%(arches)-16s %%(update)-35s" % longest_host + if options.description: + mask += " %(description)-50s" + if options.comment: + mask += " %(comment)-50s" + if options.show_channels: + mask += " %(channels)s" + for host in hosts: + print(mask % host) diff --git a/cli/koji_cli/commands/list_notifications.py b/cli/koji_cli/commands/list_notifications.py new file mode 100644 index 0000000..8344791 --- /dev/null +++ b/cli/koji_cli/commands/list_notifications.py @@ -0,0 +1,83 @@ +from __future__ import absolute_import, division + +from optparse import OptionParser + + +from koji_cli.lib import ( + activate_session, + ensure_connection, + error, + get_usage_str +) + + +def anon_handle_list_notifications(goptions, session, args): + "[monitor] List user's notifications and blocks" + usage = "usage: %prog list-notifications [options]" + parser = OptionParser(usage=get_usage_str(usage)) + parser.add_option("--mine", action="store_true", help="Just print your notifications") + parser.add_option("--user", help="Only notifications for this user") + (options, args) = parser.parse_args(args) + + if len(args) != 0: + parser.error("This command takes no arguments") + if not options.mine and not options.user: + parser.error("Use --user or --mine.") + + if options.user: + ensure_connection(session, goptions) + user = session.getUser(options.user) + if not user: + error("No such user: %s" % options.user) + user_id = user['id'] + else: + activate_session(session, goptions) + user_id = None + + mask = "%(id)6s %(tag)-25s %(package)-25s %(email)-20s %(success)-12s" + headers = {'id': 'ID', + 'tag': 'Tag', + 'package': 'Package', + 'email': 'E-mail', + 'success': 'Success-only'} + head = mask % headers + notifications = session.getBuildNotifications(user_id) + if notifications: + print('Notifications') + print(head) + print('-' * len(head)) + for notification in notifications: + if notification['tag_id']: + notification['tag'] = session.getTag(notification['tag_id'])['name'] + else: + notification['tag'] = '*' + if notification['package_id']: + notification['package'] = session.getPackage(notification['package_id'])['name'] + else: + notification['package'] = '*' + notification['success'] = ['no', 'yes'][notification['success_only']] + print(mask % notification) + else: + print('No notifications') + + print('') + + mask = "%(id)6s %(tag)-25s %(package)-25s" + head = mask % headers + blocks = session.getBuildNotificationBlocks(user_id) + if blocks: + print('Notification blocks') + print(head) + print('-' * len(head)) + for notification in blocks: + if notification['tag_id']: + notification['tag'] = session.getTag(notification['tag_id'])['name'] + else: + notification['tag'] = '*' + if notification['package_id']: + notification['package'] = session.getPackage(notification['package_id'])['name'] + else: + notification['package'] = '*' + print(mask % notification) + else: + print('No notification blocks') diff --git a/cli/koji_cli/commands/list_permissions.py b/cli/koji_cli/commands/list_permissions.py new file mode 100644 index 0000000..a7f5961 --- /dev/null +++ b/cli/koji_cli/commands/list_permissions.py @@ -0,0 +1,57 @@ +from __future__ import absolute_import, division + +from optparse import OptionParser + + +from koji_cli.lib import ( + activate_session, + error, + get_usage_str +) + + +def handle_list_permissions(goptions, session, args): + "[info] List user permissions" + usage = "usage: %prog list-permissions [options]" + parser = OptionParser(usage=get_usage_str(usage)) + parser.add_option("--user", help="List permissions for the given user") + parser.add_option("--mine", action="store_true", help="List your permissions") + parser.add_option("--quiet", action="store_true", default=goptions.quiet, + help="Do not print the header information") + (options, args) = parser.parse_args(args) + if len(args) > 0: + parser.error("This command takes no arguments") + activate_session(session, goptions) + perms = [] + if options.user: + user = session.getUser(options.user) + if not user: + error("No such user: %s" % options.user) + for p in session.getUserPerms(user['id']): + perms.append({'name': p}) + elif options.mine: + for p in session.getPerms(): + perms.append({'name': p}) + else: + for p in session.getAllPerms(): + perms.append({'name': p['name'], 'description': p['description']}) + if perms: + longest_perm = max([len(perm['name']) for perm in perms]) + perms = sorted(perms, key=lambda x: x['name']) + else: + longest_perm = 8 + if longest_perm < len('Permission name '): + longest_perm = len('Permission name ') + if not options.quiet: + hdr = '{permname:<{longest_perm}}' + hdr = hdr.format(longest_perm=longest_perm, permname='Permission name') + if perms and perms[0].get('description'): + hdr += " Description".ljust(53) + print(hdr) + print(len(hdr) * '-') + for perm in perms: + line = '{permname:<{longest_perm}}' + line = line.format(longest_perm=longest_perm, permname=perm['name']) + if perm.get('description'): + line += " %s" % perm['description'] + print(line) diff --git a/cli/koji_cli/commands/list_pkgs.py b/cli/koji_cli/commands/list_pkgs.py new file mode 100644 index 0000000..3681525 --- /dev/null +++ b/cli/koji_cli/commands/list_pkgs.py @@ -0,0 +1,104 @@ +from __future__ import absolute_import, division + +import time +from optparse import OptionParser + +import koji + +from koji_cli.lib import ( + ensure_connection, + error, + get_usage_str +) + + +def anon_handle_list_pkgs(goptions, session, args): + "[info] Print the package listing for tag or for owner" + usage = "usage: %prog list-pkgs [options]" + parser = OptionParser(usage=get_usage_str(usage)) + parser.add_option("--owner", help="Specify owner") + parser.add_option("--tag", help="Specify tag") + parser.add_option("--package", help="Specify package") + parser.add_option("--quiet", action="store_true", default=goptions.quiet, + help="Do not print header information") + parser.add_option("--noinherit", action="store_true", help="Don't follow inheritance") + parser.add_option("--show-blocked", action="store_true", help="Show blocked packages") + parser.add_option("--show-dups", action="store_true", help="Show superseded owners") + parser.add_option("--event", type='int', metavar="EVENT#", help="query at event") + parser.add_option("--ts", type='int', metavar="TIMESTAMP", + help="query at last event before timestamp") + parser.add_option("--repo", type='int', metavar="REPO#", help="query at event for a repo") + (options, args) = parser.parse_args(args) + if len(args) != 0: + parser.error("This command takes no arguments") + ensure_connection(session, goptions) + opts = {} + if options.owner: + user = session.getUser(options.owner) + if user is None: + parser.error("No such user: %s" % options.owner) + opts['userID'] = user['id'] + if options.tag: + tag = session.getTag(options.tag) + if tag is None: + parser.error("No such tag: %s" % options.tag) + opts['tagID'] = tag['id'] + if options.package: + opts['pkgID'] = options.package + allpkgs = False + if not opts: + # no limiting clauses were specified + allpkgs = True + opts['inherited'] = not options.noinherit + # hiding dups only makes sense if we're querying a tag + if options.tag: + opts['with_dups'] = options.show_dups + else: + opts['with_dups'] = True + event = koji.util.eventFromOpts(session, options) + if event: + opts['event'] = event['id'] + event['timestr'] = time.asctime(time.localtime(event['ts'])) + print("Querying at event %(id)i (%(timestr)s)" % event) + + if not opts.get('tagID') and not opts.get('userID') and \ + not opts.get('pkgID'): + if opts.get('event'): + parser.error("--event and --ts makes sense only with --tag," + " --owner or --package") + if options.show_blocked: + parser.error("--show-blocked makes sense only with --tag," + " --owner or --package") + if options.show_blocked: + opts['with_blocked'] = options.show_blocked + + try: + data = session.listPackages(**opts) + except koji.ParameterError: + del opts['with_blocked'] + data = session.listPackages(**opts) + + if not data: + error("(no matching packages)") + if not options.quiet: + if allpkgs: + print("Package") + print('-' * 23) + else: + print("%-23s %-23s %-16s %-15s" % ('Package', 'Tag', 'Extra Arches', 'Owner')) + print("%s %s %s %s" % ('-' * 23, '-' * 23, '-' * 16, '-' * 15)) + for pkg in data: + if allpkgs: + print(pkg['package_name']) + else: + if not options.show_blocked and pkg.get('blocked', False): + continue + if 'tag_id' in pkg: + if pkg['extra_arches'] is None: + pkg['extra_arches'] = "" + fmt = "%(package_name)-23s %(tag_name)-23s %(extra_arches)-16s %(owner_name)-15s" + if pkg.get('blocked', False): + fmt += " [BLOCKED]" + else: + fmt = "%(package_name)s" + print(fmt % pkg) diff --git a/cli/koji_cli/commands/list_signed.py b/cli/koji_cli/commands/list_signed.py new file mode 100644 index 0000000..f2df4b7 --- /dev/null +++ b/cli/koji_cli/commands/list_signed.py @@ -0,0 +1,103 @@ +from __future__ import absolute_import, division + +import os +from optparse import SUPPRESS_HELP, OptionParser + +import koji + +from koji_cli.lib import ( + activate_session, + get_usage_str +) + + +def handle_list_signed(goptions, session, args): + "[admin] List signed copies of rpms" + usage = "usage: %prog list-signed [options]" + description = "You must have local access to Koji's topdir filesystem." + parser = OptionParser(usage=get_usage_str(usage), description=description) + # Don't use local debug option, this one stays here for backward compatibility + # https://pagure.io/koji/issue/2084 + parser.add_option("--debug", action="store_true", default=goptions.debug, help=SUPPRESS_HELP) + parser.add_option("--key", help="Only list RPMs signed with this key") + parser.add_option("--build", help="Only list RPMs from this build") + parser.add_option("--rpm", help="Only list signed copies for this RPM") + parser.add_option("--tag", help="Only list RPMs within this tag") + (options, args) = parser.parse_args(args) + if not options.build and not options.tag and not options.rpm: + parser.error("At least one from --build, --rpm, --tag needs to be specified.") + activate_session(session, goptions) + qopts = {} + build_idx = {} + rpm_idx = {} + if options.key: + qopts['sigkey'] = options.key + + sigs = [] + if options.rpm: + rpm_info = options.rpm + try: + rpm_info = int(rpm_info) + except ValueError: + pass + rinfo = session.getRPM(rpm_info, strict=True) + rpm_idx[rinfo['id']] = rinfo + if rinfo.get('external_repo_id'): + parser.error("External rpm: %(name)s-%(version)s-%(release)s.%(arch)s@" + "%(external_repo_name)s" % rinfo) + sigs += session.queryRPMSigs(rpm_id=rinfo['id'], **qopts) + if options.build: + build = options.build + try: + build = int(build) + except ValueError: + pass + binfo = session.getBuild(build, strict=True) + build_idx[binfo['id']] = binfo + rpms = session.listRPMs(buildID=binfo['id']) + for rinfo in rpms: + rpm_idx[rinfo['id']] = rinfo + sigs += session.queryRPMSigs(rpm_id=rinfo['id'], **qopts) + if options.tag: + tag = options.tag + try: + tag = int(tag) + except ValueError: + pass + rpms, builds = session.listTaggedRPMS(tag, inherit=False, latest=False) + tagged = {} + for binfo in builds: + build_idx.setdefault(binfo['id'], binfo) + results = [] + # use batched multicall as there could be potentially a lot of results + # so we don't exhaust server resources + with session.multicall(batch=5000) as m: + for rinfo in rpms: + rpm_idx.setdefault(rinfo['id'], rinfo) + tagged[rinfo['id']] = 1 + results.append(m.queryRPMSigs(rpm_id=rinfo['id']), **qopts) + sigs += [x.result[0] for x in results] + + # Now figure out which sig entries actually have live copies + for sig in sigs: + rpm_id = sig['rpm_id'] + sigkey = sig['sigkey'] + if options.tag: + if tagged.get(rpm_id) is None: + continue + rinfo = rpm_idx.get(rpm_id) + if not rinfo: + rinfo = session.getRPM(rpm_id) + rpm_idx[rinfo['id']] = rinfo + binfo = build_idx.get(rinfo['build_id']) + if not binfo: + binfo = session.getBuild(rinfo['build_id']) + build_idx[binfo['id']] = binfo + binfo['name'] = binfo['package_name'] + builddir = koji.pathinfo.build(binfo) + signedpath = "%s/%s" % (builddir, koji.pathinfo.signed(rinfo, sigkey)) + if not os.path.exists(signedpath): + if goptions.debug: + print("No copy: %s" % signedpath) + continue + print(signedpath) diff --git a/cli/koji_cli/commands/list_tag_inheritance.py b/cli/koji_cli/commands/list_tag_inheritance.py new file mode 100644 index 0000000..7391c8a --- /dev/null +++ b/cli/koji_cli/commands/list_tag_inheritance.py @@ -0,0 +1,109 @@ +from __future__ import absolute_import, division + +import sys +import textwrap +import time +from optparse import SUPPRESS_HELP, OptionParser + +import koji + +from koji_cli.lib import ( + ensure_connection, + format_inheritance_flags, + get_usage_str, + printable_unicode, +) + + +def _printInheritance(tags, sibdepths=None, reverse=False): + if len(tags) == 0: + return + if sibdepths is None: + sibdepths = [] + currtag = tags[0] + tags = tags[1:] + if reverse: + siblings = len([tag for tag in tags if tag['parent_id'] == currtag['parent_id']]) + else: + siblings = len([tag for tag in tags if tag['child_id'] == currtag['child_id']]) + + sys.stdout.write(format_inheritance_flags(currtag)) + outdepth = 0 + for depth in sibdepths: + if depth < currtag['currdepth']: + outspacing = depth - outdepth + sys.stdout.write(' ' * (outspacing * 3 - 1)) + sys.stdout.write(printable_unicode(u'\u2502')) + outdepth = depth + + sys.stdout.write(' ' * ((currtag['currdepth'] - outdepth) * 3 - 1)) + if siblings: + sys.stdout.write(printable_unicode(u'\u251c')) + else: + sys.stdout.write(printable_unicode(u'\u2514')) + sys.stdout.write(printable_unicode(u'\u2500')) + if reverse: + sys.stdout.write('%(name)s (%(tag_id)i)\n' % currtag) + else: + sys.stdout.write('%(name)s (%(parent_id)i)\n' % currtag) + + if siblings: + if len(sibdepths) == 0 or sibdepths[-1] != currtag['currdepth']: + sibdepths.append(currtag['currdepth']) + else: + if len(sibdepths) > 0 and sibdepths[-1] == currtag['currdepth']: + sibdepths.pop() + + _printInheritance(tags, sibdepths, reverse) + + +def anon_handle_list_tag_inheritance(goptions, session, args): + "[info] Print the inheritance information for a tag" + usage = """\ + usage: %prog list-tag-inheritance [options] + + Prints tag inheritance with basic information about links. + Four flags could be seen in the output: + M - maxdepth - limits inheritance to n-levels + F - package filter (packages ignored for inheritance) + I - intransitive link - inheritance immediately stops here + N - noconfig - if tag is used in buildroot, its configuration values will not be used + + Exact values for maxdepth and package filter can be inquired by taginfo command. + """ + usage = textwrap.dedent(usage) + parser = OptionParser(usage=get_usage_str(usage)) + parser.add_option("--reverse", action="store_true", + help="Process tag's children instead of its parents") + parser.add_option("--stop", help=SUPPRESS_HELP) + parser.add_option("--jump", help=SUPPRESS_HELP) + parser.add_option("--event", type='int', metavar="EVENT#", help="query at event") + parser.add_option("--ts", type='int', metavar="TIMESTAMP", + help="query at last event before timestamp") + parser.add_option("--repo", type='int', metavar="REPO#", help="query at event for a repo") + (options, args) = parser.parse_args(args) + if len(args) != 1: + parser.error("This command takes exactly one argument: a tag name or ID") + for deprecated in ('stop', 'jump'): + if getattr(options, deprecated): + parser.error("--%s option has been removed in 1.26" % deprecated) + ensure_connection(session, goptions) + event = koji.util.eventFromOpts(session, options) + if event: + event['timestr'] = time.asctime(time.localtime(event['ts'])) + print("Querying at event %(id)i (%(timestr)s)" % event) + if event: + tag = session.getTag(args[0], event=event['id']) + else: + tag = session.getTag(args[0]) + if not tag: + parser.error("No such tag: %s" % args[0]) + + opts = {} + opts['reverse'] = options.reverse or False + if event: + opts['event'] = event['id'] + + sys.stdout.write(' %s (%i)\n' % (tag['name'], tag['id'])) + data = session.getFullInheritance(tag['id'], **opts) + _printInheritance(data, None, opts['reverse']) diff --git a/cli/koji_cli/commands/list_tagged.py b/cli/koji_cli/commands/list_tagged.py new file mode 100644 index 0000000..b71ac4b --- /dev/null +++ b/cli/koji_cli/commands/list_tagged.py @@ -0,0 +1,127 @@ +from __future__ import absolute_import, division + +import os +import time +from optparse import OptionParser + +import koji + +from koji_cli.lib import ( + ensure_connection, + get_usage_str +) + + +def anon_handle_list_tagged(goptions, session, args): + "[info] List the builds or rpms in a tag" + usage = "usage: %prog list-tagged [options] []" + parser = OptionParser(usage=get_usage_str(usage)) + parser.add_option("--arch", action="append", default=[], help="List rpms for this arch") + parser.add_option("--rpms", action="store_true", help="Show rpms instead of builds") + parser.add_option("--inherit", action="store_true", help="Follow inheritance") + parser.add_option("--latest", action="store_true", help="Only show the latest builds/rpms") + parser.add_option("--latest-n", type='int', metavar="N", + help="Only show the latest N builds/rpms") + parser.add_option("--quiet", action="store_true", default=goptions.quiet, + help="Do not print the header information") + parser.add_option("--paths", action="store_true", help="Show the file paths") + parser.add_option("--sigs", action="store_true", help="Show signatures") + parser.add_option("--type", + help="Show builds of the given type only. " + "Currently supported types: maven, win, image") + parser.add_option("--event", type='int', metavar="EVENT#", help="query at event") + parser.add_option("--ts", type='int', metavar="TIMESTAMP", + help="query at last event before timestamp") + parser.add_option("--repo", type='int', metavar="REPO#", help="query at event for a repo") + (options, args) = parser.parse_args(args) + if len(args) == 0: + parser.error("A tag name must be specified") + elif len(args) > 2: + parser.error("Only one package name may be specified") + ensure_connection(session, goptions) + pathinfo = koji.PathInfo() + package = None + if len(args) > 1: + package = args[1] + tag = args[0] + opts = {} + for key in ('latest', 'inherit'): + opts[key] = getattr(options, key) + if options.latest_n is not None: + opts['latest'] = options.latest_n + if package: + opts['package'] = package + if options.arch: + options.rpms = True + opts['arch'] = options.arch + if options.sigs: + opts['rpmsigs'] = True + options.rpms = True + if options.type: + opts['type'] = options.type + event = koji.util.eventFromOpts(session, options) + event_id = None + if event: + opts['event'] = event['id'] + event_id = event['id'] + event['timestr'] = time.asctime(time.localtime(event['ts'])) + if not options.quiet: + print("Querying at event %(id)i (%(timestr)s)" % event) + + # check if tag exist(s|ed) + taginfo = session.getTag(tag, event=event_id) + if not taginfo: + parser.error("No such tag: %s" % tag) + + if options.rpms: + rpms, builds = session.listTaggedRPMS(tag, **opts) + data = rpms + if options.paths: + build_idx = dict([(b['id'], b) for b in builds]) + for rinfo in data: + build = build_idx[rinfo['build_id']] + builddir = pathinfo.build(build) + if options.sigs: + sigkey = rinfo['sigkey'] + signedpath = os.path.join(builddir, pathinfo.signed(rinfo, sigkey)) + if os.path.exists(signedpath): + rinfo['path'] = signedpath + else: + rinfo['path'] = os.path.join(builddir, pathinfo.rpm(rinfo)) + fmt = "%(path)s" + data = [x for x in data if 'path' in x] + else: + fmt = "%(name)s-%(version)s-%(release)s.%(arch)s" + if options.sigs: + fmt = "%(sigkey)s " + fmt + else: + data = session.listTagged(tag, **opts) + if options.paths: + if options.type == 'maven': + for x in data: + x['path'] = pathinfo.mavenbuild(x) + fmt = "%(path)-40s %(tag_name)-20s %(maven_group_id)-20s " \ + "%(maven_artifact_id)-20s %(owner_name)s" + else: + for x in data: + x['path'] = pathinfo.build(x) + fmt = "%(path)-40s %(tag_name)-20s %(owner_name)s" + else: + if options.type == 'maven': + fmt = "%(nvr)-40s %(tag_name)-20s %(maven_group_id)-20s " \ + "%(maven_artifact_id)-20s %(owner_name)s" + else: + fmt = "%(nvr)-40s %(tag_name)-20s %(owner_name)s" + if not options.quiet: + if options.type == 'maven': + print("%-40s %-20s %-20s %-20s %s" % + ("Build", "Tag", "Group Id", "Artifact Id", "Built by")) + print("%s %s %s %s %s" % + ("-" * 40, "-" * 20, "-" * 20, "-" * 20, "-" * 16)) + else: + print("%-40s %-20s %s" % ("Build", "Tag", "Built by")) + print("%s %s %s" % ("-" * 40, "-" * 20, "-" * 16)) + + output = sorted([fmt % x for x in data]) + for line in output: + print(line) diff --git a/cli/koji_cli/commands/list_tags.py b/cli/koji_cli/commands/list_tags.py new file mode 100644 index 0000000..36cc72a --- /dev/null +++ b/cli/koji_cli/commands/list_tags.py @@ -0,0 +1,82 @@ +from __future__ import absolute_import, division + +import itertools +import sys +from optparse import OptionParser + +import koji + +from koji_cli.lib import ( + ensure_connection, + get_usage_str, +) + + +def anon_handle_list_tags(goptions, session, args): + "[info] Print the list of tags" + usage = "usage: %prog list-tags [options] [pattern]" + parser = OptionParser(usage=get_usage_str(usage)) + parser.add_option("--show-id", action="store_true", help="Show tag ids") + parser.add_option("--verbose", action="store_true", help="Show more information") + parser.add_option("--unlocked", action="store_true", help="Only show unlocked tags") + parser.add_option("--build", help="Show tags associated with a build") + parser.add_option("--package", help="Show tags associated with a package") + (options, patterns) = parser.parse_args(args) + ensure_connection(session, goptions) + + pkginfo = {} + buildinfo = {} + + if options.package: + pkginfo = session.getPackage(options.package) + if not pkginfo: + parser.error("No such package: %s" % options.package) + + if options.build: + buildinfo = session.getBuild(options.build) + if not buildinfo: + parser.error("No such build: %s" % options.build) + + if not patterns: + # list everything if no pattern is supplied + tags = session.listTags(build=buildinfo.get('id', None), + package=pkginfo.get('id', None)) + else: + # The hub may not support the pattern option. We try with that first + # and fall back to the old way. + fallback = False + try: + tags = [] + with session.multicall(strict=True) as m: + for pattern in patterns: + tags.append(m.listTags(build=buildinfo.get('id', None), + package=pkginfo.get('id', None), + pattern=pattern)) + tags = list(itertools.chain(*[t.result for t in tags])) + except koji.ParameterError: + fallback = True + if fallback: + # without the pattern option, we have to filter client side + tags = session.listTags(buildinfo.get('id', None), pkginfo.get('id', None)) + tags = [t for t in tags if koji.util.multi_fnmatch(t['name'], patterns)] + + tags.sort(key=lambda x: x['name']) + # if options.verbose: + # fmt = "%(name)s [%(id)i] %(perm)s %(locked)s %(arches)s" + if options.show_id: + fmt = "%(name)s [%(id)i]" + else: + fmt = "%(name)s" + for tag in tags: + if options.unlocked: + if tag['locked'] or tag['perm']: + continue + if not options.verbose: + print(fmt % tag) + else: + sys.stdout.write(fmt % tag) + if tag['locked']: + sys.stdout.write(' [LOCKED]') + if tag['perm']: + sys.stdout.write(' [%(perm)s perm required]' % tag) + print('') diff --git a/cli/koji_cli/commands/list_targets.py b/cli/koji_cli/commands/list_targets.py new file mode 100644 index 0000000..37861e3 --- /dev/null +++ b/cli/koji_cli/commands/list_targets.py @@ -0,0 +1,40 @@ +from __future__ import absolute_import, division + +from optparse import OptionParser + + +from koji_cli.lib import ( + ensure_connection, + get_usage_str +) + + +def anon_handle_list_targets(goptions, session, args): + "[info] List the build targets" + usage = "usage: %prog list-targets [options]" + parser = OptionParser(usage=get_usage_str(usage)) + parser.add_option("--name", help="Specify the build target name") + parser.add_option("--quiet", action="store_true", default=goptions.quiet, + help="Do not print the header information") + (options, args) = parser.parse_args(args) + + if len(args) != 0: + parser.error("This command takes no arguments") + ensure_connection(session, goptions) + + targets = session.getBuildTargets(options.name) + if len(targets) == 0: + if options.name: + parser.error('No such build target: %s' % options.name) + else: + parser.error('No targets were found') + + fmt = "%(name)-30s %(build_tag_name)-30s %(dest_tag_name)-30s" + if not options.quiet: + print("%-30s %-30s %-30s" % ('Name', 'Buildroot', 'Destination')) + print("-" * 93) + tmp_list = sorted([(x['name'], x) for x in targets]) + targets = [x[1] for x in tmp_list] + for target in targets: + print(fmt % target) + # pprint.pprint(session.getBuildTargets()) diff --git a/cli/koji_cli/commands/list_tasks.py b/cli/koji_cli/commands/list_tasks.py new file mode 100644 index 0000000..657d613 --- /dev/null +++ b/cli/koji_cli/commands/list_tasks.py @@ -0,0 +1,52 @@ +from __future__ import absolute_import, division + +from optparse import OptionParser + + +from koji_cli.lib import ( + TimeOption, + _list_tasks, + activate_session, + get_usage_str, + print_task_headers, + print_task_recurse +) + + +def handle_list_tasks(goptions, session, args): + "[info] Print the list of tasks" + usage = "usage: %prog list-tasks [options]" + parser = OptionParser(usage=get_usage_str(usage), option_class=TimeOption) + parser.add_option("--mine", action="store_true", help="Just print your tasks") + parser.add_option("--user", help="Only tasks for this user") + parser.add_option("--arch", help="Only tasks for this architecture") + parser.add_option("--method", help="Only tasks of this method") + parser.add_option("--channel", help="Only tasks in this channel") + parser.add_option("--host", help="Only tasks for this host") + parser.add_option("--quiet", action="store_true", default=goptions.quiet, + help="Do not display the column headers") + parser.add_option("--before", type="time", + help="List tasks completed before this time, " + TimeOption.get_help()) + parser.add_option("--after", type="time", + help="List tasks completed after this time (same format as for --before") + parser.add_option("--all", action="store_true", + help="List also finished tasks (valid only with --after)") + (options, args) = parser.parse_args(args) + if len(args) != 0: + parser.error("This command takes no arguments") + + if options.all and not options.after: + parser.error("--all must be used with --after") + + activate_session(session, goptions) + tasklist = _list_tasks(options, session) + if not tasklist: + print("(no tasks)") + return + if not options.quiet: + print_task_headers() + for t in tasklist: + if t.get('sub'): + # this subtask will appear under another task + continue + print_task_recurse(t) diff --git a/cli/koji_cli/commands/list_untagged.py b/cli/koji_cli/commands/list_untagged.py new file mode 100644 index 0000000..8c82e47 --- /dev/null +++ b/cli/koji_cli/commands/list_untagged.py @@ -0,0 +1,63 @@ +from __future__ import absolute_import, division + +from optparse import OptionParser + +import koji + +from koji_cli.lib import ( + ensure_connection, + error, + get_usage_str +) + + +def anon_handle_list_untagged(goptions, session, args): + "[info] List untagged builds" + usage = "usage: %prog list-untagged [options] []" + parser = OptionParser(usage=get_usage_str(usage)) + parser.add_option("--paths", action="store_true", help="Show the file paths") + parser.add_option("--show-references", action="store_true", help="Show build references") + (options, args) = parser.parse_args(args) + if len(args) > 1: + parser.error("Only one package name may be specified") + ensure_connection(session, goptions) + package = None + if len(args) > 0: + package = args[0] + opts = {} + if package: + package_id = session.getPackageID(package) + if package_id is None: + error("No such package: %s" % package) + opts['name'] = package + pathinfo = koji.PathInfo() + + data = session.untaggedBuilds(**opts) + if options.show_references: + print("(Showing build references)") + references = {} + with session.multicall(strict=True, batch=10000) as m: + for build in data: + references[build['id']] = m.buildReferences(build['id']) + + for build in data: + refs = references[build['id']].result + r = [] + if refs.get('rpms'): + r.append("rpms: %s" % refs['rpms']) + if refs.get('component_of'): + r.append("images/archives: %s" % refs['component_of']) + if refs.get('archives'): + r.append("archives buildroots: %s" % refs['archives']) + build['refs'] = ', '.join(r) + if options.paths: + for x in data: + x['path'] = pathinfo.build(x) + fmt = "%(path)s" + else: + fmt = "%(name)s-%(version)s-%(release)s" + if options.show_references: + fmt = fmt + " %(refs)s" + output = sorted([fmt % x for x in data]) + for line in output: + print(line) diff --git a/cli/koji_cli/commands/list_volumes.py b/cli/koji_cli/commands/list_volumes.py new file mode 100644 index 0000000..45fe5f7 --- /dev/null +++ b/cli/koji_cli/commands/list_volumes.py @@ -0,0 +1,19 @@ +from __future__ import absolute_import, division + +from optparse import OptionParser + + +from koji_cli.lib import ( + ensure_connection, + get_usage_str +) + + +def anon_handle_list_volumes(goptions, session, args): + "[info] List storage volumes" + usage = "usage: %prog list-volumes" + parser = OptionParser(usage=get_usage_str(usage)) + (options, args) = parser.parse_args(args) + ensure_connection(session, goptions) + for volinfo in session.listVolumes(): + print(volinfo['name']) diff --git a/cli/koji_cli/commands/lock_tag.py b/cli/koji_cli/commands/lock_tag.py new file mode 100644 index 0000000..f6e961c --- /dev/null +++ b/cli/koji_cli/commands/lock_tag.py @@ -0,0 +1,59 @@ +from __future__ import absolute_import, division + +import fnmatch +from optparse import OptionParser + + +from koji_cli.lib import ( + activate_session, + get_usage_str +) + + +def handle_lock_tag(goptions, session, args): + "[admin] Lock a tag" + usage = "usage: %prog lock-tag [options] [ ...]" + parser = OptionParser(usage=get_usage_str(usage)) + parser.add_option("--perm", help="Specify permission requirement") + parser.add_option("--glob", action="store_true", help="Treat args as glob patterns") + parser.add_option("--master", action="store_true", help="Lock the master lock") + parser.add_option("-n", "--test", action="store_true", help="Test mode") + (options, args) = parser.parse_args(args) + if len(args) < 1: + parser.error("Please specify a tag") + activate_session(session, goptions) + pdata = session.getAllPerms() + perm_ids = dict([(p['name'], p['id']) for p in pdata]) + perm = options.perm + if perm is None: + perm = 'admin' + perm_id = perm_ids[perm] + if options.glob: + selected = [] + for tag in session.listTags(): + for pattern in args: + if fnmatch.fnmatch(tag['name'], pattern): + selected.append(tag) + break + if not selected: + print("No tags matched") + else: + selected = [session.getTag(name, strict=True) for name in args] + for tag in selected: + if options.master: + # set the master lock + if tag['locked']: + print("Tag %s: master lock already set" % tag['name']) + continue + elif options.test: + print("Would have set master lock for: %s" % tag['name']) + continue + session.editTag2(tag['id'], locked=True) + else: + if tag['perm_id'] == perm_id: + print("Tag %s: %s permission already required" % (tag['name'], perm)) + continue + elif options.test: + print("Would have set permission requirement %s for tag %s" % (perm, tag['name'])) + continue + session.editTag2(tag['id'], perm_id=perm_id) diff --git a/cli/koji_cli/commands/make_task.py b/cli/koji_cli/commands/make_task.py new file mode 100644 index 0000000..9858159 --- /dev/null +++ b/cli/koji_cli/commands/make_task.py @@ -0,0 +1,44 @@ +from __future__ import absolute_import, division + +from optparse import OptionParser + +from six.moves import map + + +from koji_cli.lib import ( + _running_in_bg, + activate_session, + arg_filter, + get_usage_str, + watch_tasks +) + + +def handle_make_task(goptions, session, args): + "[admin] Create an arbitrary task" + usage = "usage: %prog make-task [options] [ ...]" + parser = OptionParser(usage=get_usage_str(usage)) + parser.add_option("--channel", help="set channel") + parser.add_option("--priority", help="set priority") + parser.add_option("--watch", action="store_true", help="watch the task") + parser.add_option("--arch", help="set arch") + (options, args) = parser.parse_args(args) + if len(args) < 1: + parser.error("Please specify task method at least") + + activate_session(session, goptions) + taskopts = {} + for key in ('channel', 'priority', 'arch'): + value = getattr(options, key, None) + if value is not None: + taskopts[key] = value + task_id = session.makeTask(method=args[0], + arglist=list(map(arg_filter, args[1:])), + **taskopts) + print("Created task id %d" % task_id) + if _running_in_bg() or not options.watch: + return + else: + session.logout() + return watch_tasks(session, [task_id], quiet=goptions.quiet, + poll_interval=goptions.poll_interval, topurl=goptions.topurl) diff --git a/cli/koji_cli/commands/maven_build.py b/cli/koji_cli/commands/maven_build.py new file mode 100644 index 0000000..45d2b96 --- /dev/null +++ b/cli/koji_cli/commands/maven_build.py @@ -0,0 +1,101 @@ +from __future__ import absolute_import, division + +from optparse import OptionParser + +import koji +from koji.util import to_list + +from koji_cli.lib import ( + _running_in_bg, + activate_session, + get_usage_str, + watch_tasks +) + + +def handle_maven_build(options, session, args): + "[build] Build a Maven package from source" + usage = "usage: %prog maven-build [options] " + usage += "\n %prog maven-build --ini=CONFIG... [options] " + parser = OptionParser(usage=get_usage_str(usage)) + parser.add_option("--patches", action="store", metavar="URL", + help="SCM URL of a directory containing patches to apply to the sources " + "before building") + parser.add_option("-G", "--goal", action="append", dest="goals", metavar="GOAL", default=[], + help="Additional goal to run before \"deploy\"") + parser.add_option("-P", "--profile", action="append", dest="profiles", metavar="PROFILE", + default=[], help="Enable a profile for the Maven build") + parser.add_option("-D", "--property", action="append", dest="properties", metavar="NAME=VALUE", + default=[], help="Pass a system property to the Maven build") + parser.add_option("-E", "--env", action="append", dest="envs", metavar="NAME=VALUE", + default=[], help="Set an environment variable") + parser.add_option("-p", "--package", action="append", dest="packages", metavar="PACKAGE", + default=[], help="Install an additional package into the buildroot") + parser.add_option("-J", "--jvm-option", action="append", dest="jvm_options", metavar="OPTION", + default=[], help="Pass a command-line option to the JVM") + parser.add_option("-M", "--maven-option", action="append", dest="maven_options", + metavar="OPTION", default=[], help="Pass a command-line option to Maven") + parser.add_option("--ini", action="append", dest="inis", metavar="CONFIG", default=[], + help="Pass build parameters via a .ini file") + parser.add_option("-s", "--section", help="Get build parameters from this section of the .ini") + parser.add_option("--debug", action="store_true", help="Run Maven build in debug mode") + parser.add_option("--specfile", action="store", metavar="URL", + help="SCM URL of a spec file fragment to use to generate wrapper RPMs") + parser.add_option("--skip-tag", action="store_true", help="Do not attempt to tag package") + parser.add_option("--scratch", action="store_true", help="Perform a scratch build") + parser.add_option("--wait", action="store_true", + help="Wait on build, even if running in the background") + parser.add_option("--nowait", action="store_false", dest="wait", help="Don't wait on build") + parser.add_option("--quiet", action="store_true", + help="Do not print the task information", default=options.quiet) + parser.add_option("--background", action="store_true", + help="Run the build at a lower priority") + (build_opts, args) = parser.parse_args(args) + if build_opts.inis: + if len(args) != 1: + parser.error("Exactly one argument (a build target) is required") + else: + if len(args) != 2: + parser.error("Exactly two arguments (a build target and a SCM URL) are required") + activate_session(session, options) + target = args[0] + build_target = session.getBuildTarget(target) + if not build_target: + parser.error("No such build target: %s" % target) + dest_tag = session.getTag(build_target['dest_tag']) + if not dest_tag: + parser.error("No such destination tag: %s" % build_target['dest_tag_name']) + if dest_tag['locked'] and not build_opts.scratch: + parser.error("Destination tag %s is locked" % dest_tag['name']) + if build_opts.inis: + try: + params = koji.util.parse_maven_param(build_opts.inis, scratch=build_opts.scratch, + section=build_opts.section) + except ValueError as e: + parser.error(e.args[0]) + opts = to_list(params.values())[0] + if opts.pop('type', 'maven') != 'maven': + parser.error("Section %s does not contain a maven-build config" % + to_list(params.keys())[0]) + source = opts.pop('scmurl') + else: + source = args[1] + opts = koji.util.maven_opts(build_opts, scratch=build_opts.scratch) + if '://' not in source: + parser.error("No such SCM URL: %s" % source) + if build_opts.debug: + opts.setdefault('maven_options', []).append('--debug') + if build_opts.skip_tag: + opts['skip_tag'] = True + priority = None + if build_opts.background: + # relative to koji.PRIO_DEFAULT + priority = 5 + task_id = session.mavenBuild(source, target, opts, priority=priority) + if not build_opts.quiet: + print("Created task: %d" % task_id) + print("Task info: %s/taskinfo?taskID=%s" % (options.weburl, task_id)) + if build_opts.wait or (build_opts.wait is None and not _running_in_bg()): + session.logout() + return watch_tasks(session, [task_id], quiet=build_opts.quiet, + poll_interval=options.poll_interval, topurl=options.topurl) diff --git a/cli/koji_cli/commands/maven_chain.py b/cli/koji_cli/commands/maven_chain.py new file mode 100644 index 0000000..3929556 --- /dev/null +++ b/cli/koji_cli/commands/maven_chain.py @@ -0,0 +1,59 @@ +from __future__ import absolute_import, division + +from optparse import OptionParser + +import koji + +from koji_cli.lib import ( + _running_in_bg, + activate_session, + get_usage_str, + watch_tasks +) + + +def handle_maven_chain(options, session, args): + "[build] Run a set of Maven builds in dependency order" + usage = "usage: %prog maven-chain [options] [ ...]" + parser = OptionParser(usage=get_usage_str(usage)) + parser.add_option("--skip-tag", action="store_true", help="Do not attempt to tag builds") + parser.add_option("--scratch", action="store_true", help="Perform scratch builds") + parser.add_option("--debug", action="store_true", help="Run Maven build in debug mode") + parser.add_option("--force", action="store_true", help="Force rebuilds of all packages") + parser.add_option("--wait", action="store_true", + help="Wait on build, even if running in the background") + parser.add_option("--nowait", action="store_false", dest="wait", help="Don't wait on build") + parser.add_option("--background", action="store_true", + help="Run the build at a lower priority") + (build_opts, args) = parser.parse_args(args) + if len(args) < 2: + parser.error("Two arguments (a build target and a config file) are required") + activate_session(session, options) + target = args[0] + build_target = session.getBuildTarget(target) + if not build_target: + parser.error("No such build target: %s" % target) + dest_tag = session.getTag(build_target['dest_tag']) + if not dest_tag: + parser.error("No such destination tag: %s" % build_target['dest_tag_name']) + if dest_tag['locked'] and not build_opts.scratch: + parser.error("Destination tag %s is locked" % dest_tag['name']) + opts = {} + for key in ('skip_tag', 'scratch', 'debug', 'force'): + val = getattr(build_opts, key) + if val: + opts[key] = val + try: + builds = koji.util.parse_maven_chain(args[1:], scratch=opts.get('scratch')) + except ValueError as e: + parser.error(e.args[0]) + priority = None + if build_opts.background: + priority = 5 + task_id = session.chainMaven(builds, target, opts, priority=priority) + print("Created task: %d" % task_id) + print("Task info: %s/taskinfo?taskID=%s" % (options.weburl, task_id)) + if build_opts.wait or (build_opts.wait is None and not _running_in_bg()): + session.logout() + return watch_tasks(session, [task_id], quiet=options.quiet, + poll_interval=options.poll_interval, topurl=options.topurl) diff --git a/cli/koji_cli/commands/mock_config.py b/cli/koji_cli/commands/mock_config.py new file mode 100644 index 0000000..8f04a85 --- /dev/null +++ b/cli/koji_cli/commands/mock_config.py @@ -0,0 +1,165 @@ +from __future__ import absolute_import, division + +from optparse import OptionParser + +import koji + +from koji_cli.lib import ( + ensure_connection, + error, + get_usage_str, + warn +) + + +def anon_handle_mock_config(goptions, session, args): + "[info] Create a mock config" + usage = "usage: %prog mock-config [options]" + parser = OptionParser(usage=get_usage_str(usage)) + parser.add_option("-a", "--arch", help="Specify the arch") + parser.add_option("-n", "--name", help="Specify the name for the buildroot") + parser.add_option("--tag", help="Create a mock config for a tag") + parser.add_option("--target", help="Create a mock config for a build target") + parser.add_option("--task", help="Duplicate the mock config of a previous task") + parser.add_option("--latest", action="store_true", help="use the latest redirect url") + parser.add_option("--buildroot", + help="Duplicate the mock config for the specified buildroot id") + parser.add_option("--mockdir", default="/var/lib/mock", metavar="DIR", help="Specify mockdir") + parser.add_option("--topdir", metavar="DIR", + help="Specify topdir, topdir tops the topurl") + parser.add_option("--topurl", metavar="URL", + help="URL under which Koji files are accessible, " + "when topdir is specified, topdir tops the topurl") + parser.add_option("--distribution", default="Koji Testing", + help="Change the distribution macro") + parser.add_option("--yum-proxy", help="Specify a yum proxy") + parser.add_option("-o", metavar="FILE", dest="ofile", help="Output to a file") + (options, args) = parser.parse_args(args) + ensure_connection(session, goptions) + if args: + # for historical reasons, we also accept buildroot name as first arg + if not options.name: + options.name = args[0] + else: + parser.error("Name already specified via option") + arch = None + opts = {} + for k in ('topdir', 'topurl', 'distribution', 'mockdir', 'yum_proxy'): + if hasattr(options, k): + if getattr(options, k) is not None: + opts[k] = getattr(options, k) + if opts.get('topdir') and opts.get('topurl'): + del opts['topurl'] + if not opts.get('topdir') and not opts.get('topurl'): + opts['topurl'] = goptions.topurl + if options.buildroot: + try: + br_id = int(options.buildroot) + except ValueError: + parser.error("Buildroot id must be an integer") + brootinfo = session.getBuildroot(br_id) + if brootinfo is None: + error("No such buildroot: %r" % br_id) + if options.latest: + opts['repoid'] = 'latest' + else: + opts['repoid'] = brootinfo['repo_id'] + opts['tag_name'] = brootinfo['tag_name'] + arch = brootinfo['arch'] + elif options.task: + try: + task_id = int(options.task) + except ValueError: + parser.error("Task id must be an integer") + broots = session.listBuildroots(taskID=task_id) + if not broots: + error("No buildroots for task %s (or no such task)" % options.task) + if len(broots) > 1: + print("Multiple buildroots found: %s" % [br['id'] for br in broots]) + brootinfo = broots[-1] + if options.latest: + opts['repoid'] = 'latest' + else: + opts['repoid'] = brootinfo['repo_id'] + opts['tag_name'] = brootinfo['tag_name'] + arch = brootinfo['arch'] + if not options.name: + options.name = "%s-task_%i" % (opts['tag_name'], task_id) + elif options.tag: + if not options.arch: + error("Please specify an arch") + tag = session.getTag(options.tag) + if not tag: + parser.error("No such tag: %s" % options.tag) + arch = options.arch + config = session.getBuildConfig(tag['id']) + if not config: + error("Could not get config info for tag: %(name)s" % tag) + opts['tag_name'] = tag['name'] + if options.latest: + opts['repoid'] = 'latest' + else: + repo = session.getRepo(config['id']) + if not repo: + error("Could not get a repo for tag: %(name)s" % tag) + opts['repoid'] = repo['id'] + elif options.target: + if not options.arch: + error("Please specify an arch") + arch = options.arch + target = session.getBuildTarget(options.target) + if not target: + parser.error("No such build target: %s" % options.target) + opts['tag_name'] = target['build_tag_name'] + if options.latest: + opts['repoid'] = 'latest' + else: + repo = session.getRepo(target['build_tag']) + if not repo: + error("Could not get a repo for tag: %s" % opts['tag_name']) + opts['repoid'] = repo['id'] + else: + parser.error("Please specify one of: --tag, --target, --task, --buildroot") + if options.name: + name = options.name + else: + name = "%(tag_name)s-repo_%(repoid)s" % opts + + event = None + if opts['repoid'] != 'latest': + event = session.repoInfo(opts['repoid'])['create_event'] + buildcfg = session.getBuildConfig(opts['tag_name'], event=event) + if arch: + if not buildcfg['arches']: + warn("Tag %s has an empty arch list" % opts['tag_name']) + elif arch not in buildcfg['arches']: + warn('%s is not in the list of tag arches' % arch) + if 'mock.forcearch' in buildcfg['extra']: + if bool(buildcfg['extra']['mock.forcearch']): + opts['forcearch'] = arch + if 'mock.package_manager' in buildcfg['extra']: + opts['package_manager'] = buildcfg['extra']['mock.package_manager'] + if 'mock.yum.module_hotfixes' in buildcfg['extra']: + opts['module_hotfixes'] = buildcfg['extra']['mock.yum.module_hotfixes'] + if 'mock.yum.best' in buildcfg['extra']: + opts['yum_best'] = int(buildcfg['extra']['mock.yum.best']) + if 'mock.bootstrap_image' in buildcfg['extra']: + opts['use_bootstrap_image'] = True + opts['bootstrap_image'] = buildcfg['extra']['mock.bootstrap_image'] + if 'mock.use_bootstrap' in buildcfg['extra']: + opts['use_bootstrap'] = buildcfg['extra']['mock.use_bootstrap'] + if 'mock.module_setup_commands' in buildcfg['extra']: + opts['module_setup_commands'] = buildcfg['extra']['mock.module_setup_commands'] + if 'mock.releasever' in buildcfg['extra']: + opts['releasever'] = buildcfg['extra']['mock.releasever'] + opts['tag_macros'] = {} + for key in buildcfg['extra']: + if key.startswith('rpm.macro.'): + macro = '%' + key[10:] + opts['tag_macros'][macro] = buildcfg['extra'][key] + output = koji.genMockConfig(name, arch, **opts) + if options.ofile: + with open(options.ofile, 'wt') as fo: + fo.write(output) + else: + print(output) diff --git a/cli/koji_cli/commands/moshimoshi.py b/cli/koji_cli/commands/moshimoshi.py new file mode 100644 index 0000000..1cdbaa6 --- /dev/null +++ b/cli/koji_cli/commands/moshimoshi.py @@ -0,0 +1,39 @@ +from __future__ import absolute_import, division + +import random +from optparse import OptionParser + +import koji + +from koji_cli.lib import ( + activate_session, + get_usage_str, + greetings, + printable_unicode, +) + + +def handle_moshimoshi(options, session, args): + "[misc] Introduce yourself" + usage = "usage: %prog moshimoshi [options]" + parser = OptionParser(usage=get_usage_str(usage)) + (opts, args) = parser.parse_args(args) + if len(args) != 0: + parser.error("This command takes no arguments") + activate_session(session, options) + u = session.getLoggedInUser() + if not u: + print("Not authenticated") + u = {'name': 'anonymous user'} + print("%s, %s!" % (printable_unicode(random.choice(greetings)), u["name"])) + print("") + print("You are using the hub at %s" % session.baseurl) + authtype = u.get('authtype', getattr(session, 'authtype', None)) + if authtype == koji.AUTHTYPE_NORMAL: + print("Authenticated via password") + elif authtype == koji.AUTHTYPE_GSSAPI: + print("Authenticated via GSSAPI") + elif authtype == koji.AUTHTYPE_KERB: + print("Authenticated via Kerberos principal %s" % session.krb_principal) + elif authtype == koji.AUTHTYPE_SSL: + print("Authenticated via client certificate %s" % options.cert) diff --git a/cli/koji_cli/commands/move_build.py b/cli/koji_cli/commands/move_build.py new file mode 100644 index 0000000..f65a329 --- /dev/null +++ b/cli/koji_cli/commands/move_build.py @@ -0,0 +1,64 @@ +from __future__ import absolute_import, division + +from optparse import OptionParser + +import koji + +from koji_cli.lib import ( + _running_in_bg, + activate_session, + get_usage_str, + watch_tasks +) + + +def handle_move_build(opts, session, args): + "[bind] 'Move' one or more builds between tags" + usage = "usage: %prog move-build [options] [ ...]" + parser = OptionParser(usage=get_usage_str(usage)) + parser.add_option("--force", action="store_true", help="force operation") + parser.add_option("--wait", action="store_true", + help="Wait on tasks, even if running in the background") + parser.add_option("--nowait", action="store_false", dest="wait", + help="Do not wait on tasks") + parser.add_option("--all", action="store_true", + help="move all instances of a package, 's are package names") + (options, args) = parser.parse_args(args) + if len(args) < 3: + if options.all: + parser.error( + "This command, with --all, takes at least three arguments: two tags and one or " + "more package names") + else: + parser.error( + "This command takes at least three arguments: two tags and one or more package " + "n-v-r's") + activate_session(session, opts) + tasks = [] + builds = [] + + if options.all: + for arg in args[2:]: + pkg = session.getPackage(arg) + if not pkg: + print("No such package: %s, skipping." % arg) + continue + tasklist = session.moveAllBuilds(args[0], args[1], arg, options.force) + tasks.extend(tasklist) + else: + for arg in args[2:]: + build = session.getBuild(arg) + if not build: + print("No such build: %s, skipping." % arg) + continue + if build not in builds: + builds.append(build) + + for build in builds: + task_id = session.moveBuild(args[0], args[1], build['id'], options.force) + tasks.append(task_id) + print("Created task %d, moving %s" % (task_id, koji.buildLabel(build))) + if options.wait or (options.wait is None and not _running_in_bg()): + session.logout() + return watch_tasks(session, tasks, quiet=opts.quiet, + poll_interval=opts.poll_interval, topurl=opts.topurl) diff --git a/cli/koji_cli/commands/prune_signed_copies.py b/cli/koji_cli/commands/prune_signed_copies.py new file mode 100644 index 0000000..ad2aa04 --- /dev/null +++ b/cli/koji_cli/commands/prune_signed_copies.py @@ -0,0 +1,370 @@ +from __future__ import absolute_import, division + +import fnmatch +import os +import stat +import time +from optparse import SUPPRESS_HELP, OptionParser + +import six +from six.moves import range, zip + +import koji +from koji.util import to_list + +from koji_cli.lib import ( + activate_session, + get_usage_str, + warn +) + + +def handle_prune_signed_copies(goptions, session, args): + "[admin] Prune signed copies" + usage = "usage: %prog prune-signed-copies [options]" + parser = OptionParser(usage=get_usage_str(usage)) + parser.add_option("-n", "--test", action="store_true", help="Test mode") + parser.add_option("-v", "--verbose", action="store_true", help="Be more verbose") + parser.add_option("--days", type="int", default=5, help="Timeout before clearing") + parser.add_option("-p", "--package", "--pkg", help="Limit to a single package") + parser.add_option("-b", "--build", help="Limit to a single build") + parser.add_option("-i", "--ignore-tag", action="append", default=[], + help="Ignore these tags when considering whether a build is/was latest") + parser.add_option("--ignore-tag-file", + help="File to read tag ignore patterns from") + parser.add_option("-r", "--protect-tag", action="append", default=[], + help="Do not prune signed copies from matching tags") + parser.add_option("--protect-tag-file", + help="File to read tag protect patterns from") + parser.add_option("--trashcan-tag", default="trashcan", help="Specify trashcan tag") + # Don't use local debug option, this one stays here for backward compatibility + # https://pagure.io/koji/issue/2084 + parser.add_option("--debug", action="store_true", default=goptions.debug, help=SUPPRESS_HELP) + (options, args) = parser.parse_args(args) + # different ideas/modes + # 1) remove all signed copies of builds that are not latest for some tag + # 2) remove signed copies when a 'better' signature is available + # 3) for a specified tag, remove all signed copies that are not latest (w/ inheritance) + # 4) for a specified tag, remove all signed copies (no inheritance) + # (but skip builds that are multiply tagged) + + # for now, we're just implementing mode #1 + # (with the modification that we check to see if the build was latest within + # the last N days) + if options.ignore_tag_file: + with open(options.ignore_tag_file) as fo: + options.ignore_tag.extend([line.strip() for line in fo.readlines()]) + if options.protect_tag_file: + with open(options.protect_tag_file) as fo: + options.protect_tag.extend([line.strip() for line in fo.readlines()]) + if options.debug: + options.verbose = True + cutoff_ts = time.time() - options.days * 24 * 3600 + if options.debug: + print("Cutoff date: %s" % time.asctime(time.localtime(cutoff_ts))) + activate_session(session, goptions) + if not options.build: + if options.verbose: + print("Getting builds...") + qopts = { + 'state': koji.BUILD_STATES['COMPLETE'], + 'queryOpts': { + 'limit': 50000, + 'offset': 0, + 'order': 'build_id', + } + } + if options.package: + pkginfo = session.getPackage(options.package) + qopts['packageID'] = pkginfo['id'] + builds = [] + while True: + chunk = [(b['nvr'], b) for b in session.listBuilds(**qopts)] + if not chunk: + break + builds.extend(chunk) + qopts['queryOpts']['offset'] += qopts['queryOpts']['limit'] + if options.verbose: + print("...got %i builds" % len(builds)) + builds.sort() + else: + # single build + binfo = session.getBuild(options.build) + if not binfo: + parser.error('No such build: %s' % options.build) + builds = [("%(name)s-%(version)s-%(release)s" % binfo, binfo)] + total_files = 0 + total_space = 0 + + def _histline(event_id, x): + if event_id == x['revoke_event']: + ts = x['revoke_ts'] + fmt = "Untagged %(name)s-%(version)s-%(release)s from %(tag_name)s" + elif event_id == x['create_event']: + ts = x['create_ts'] + fmt = "Tagged %(name)s-%(version)s-%(release)s with %(tag_name)s" + if x['active']: + fmt += " [still active]" + else: + raise koji.GenericError("No such event: (%r, %r)" % (event_id, x)) + time_str = time.asctime(time.localtime(ts)) + return "%s: %s" % (time_str, fmt % x) + for nvr, binfo in builds: + # listBuilds returns slightly different data than normal + if 'id' not in binfo: + binfo['id'] = binfo['build_id'] + if 'name' not in binfo: + binfo['name'] = binfo['package_name'] + if options.debug: + print("DEBUG: %s" % nvr) + # see how recently this build was latest for a tag + is_latest = False + is_protected = False + last_latest = None + tags = {} + for entry in session.queryHistory(build=binfo['id'])['tag_listing']: + # we used queryHistory rather than listTags so we can consider tags + # that the build was recently untagged from + tags.setdefault(entry['tag.name'], 1) + if options.debug: + print("Tags: %s" % to_list(tags.keys())) + for tag_name in tags: + if tag_name == options.trashcan_tag: + if options.debug: + print("Ignoring trashcan tag for build %s" % nvr) + continue + ignore_tag = False + for pattern in options.ignore_tag: + if fnmatch.fnmatch(tag_name, pattern): + if options.debug: + print("Ignoring tag %s for build %s" % (tag_name, nvr)) + ignore_tag = True + break + if ignore_tag: + continue + # in order to determine how recently this build was latest, we have + # to look at the tagging history. + hist = session.queryHistory(tag=tag_name, package=binfo['name'])['tag_listing'] + if not hist: + # really shouldn't happen + raise koji.GenericError("No history found for %s in %s" % (nvr, tag_name)) + timeline = [] + for x in hist: + # note that for revoked entries, we're effectively splitting them into + # two parts: creation and revocation. + timeline.append((x['create_event'], 1, x)) + # at the same event, revokes happen first + if x['revoke_event'] is not None: + timeline.append((x['revoke_event'], 0, x)) + timeline.sort(key=lambda entry: entry[:2]) + # find most recent creation entry for our build and crop there + latest_ts = None + for i in range(len(timeline) - 1, -1, -1): + # searching in reverse cronological order + event_id, is_create, entry = timeline[i] + if entry['build_id'] == binfo['id'] and is_create: + latest_ts = event_id + break + if not latest_ts: + # really shouldn't happen + raise koji.GenericError("No creation event found for %s in %s" % (nvr, tag_name)) + our_entry = entry + if options.debug: + print(_histline(event_id, our_entry)) + # now go through the events since most recent creation entry + timeline = timeline[i + 1:] + if not timeline: + is_latest = True + if options.debug: + print("%s is latest in tag %s" % (nvr, tag_name)) + break + # before we go any further, is this a protected tag? + protect_tag = False + for pattern in options.protect_tag: + if fnmatch.fnmatch(tag_name, pattern): + protect_tag = True + break + if protect_tag: + # we use the same time limit as for the latest calculation + # if this build was in this tag within that limit, then we will + # not prune its signed copies + if our_entry['revoke_event'] is None: + # we're still tagged with a protected tag + if options.debug: + print("Build %s has protected tag %s" % (nvr, tag_name)) + is_protected = True + break + elif our_entry['revoke_ts'] > cutoff_ts: + # we were still tagged here sometime before the cutoff + if options.debug: + print("Build %s had protected tag %s until %s" + % (nvr, tag_name, + time.asctime(time.localtime(our_entry['revoke_ts'])))) + is_protected = True + break + replaced_ts = None + revoke_ts = None + others = {} + for event_id, is_create, entry in timeline: + # So two things can knock this build from the title of latest: + # - it could be untagged (entry revoked) + # - another build could become latest (replaced) + # Note however that if the superceding entry is itself revoked, then + # our build could become latest again + if options.debug: + print(_histline(event_id, entry)) + if entry['build_id'] == binfo['id']: + if is_create: + # shouldn't happen + raise koji.GenericError("Duplicate creation event found for %s in %s" + % (nvr, tag_name)) + else: + # we've been revoked + revoke_ts = entry['revoke_ts'] + break + else: + if is_create: + # this build has become latest + replaced_ts = entry['create_ts'] + if entry['active']: + # this entry not revoked yet, so we're done for this tag + break + # since this entry is revoked later, our build might eventually be + # uncovered, so we have to keep looking + others[entry['build_id']] = 1 + else: + # other build revoked + # see if our build has resurfaced + if entry['build_id'] in others: + del others[entry['build_id']] + if replaced_ts is not None and not others: + # we've become latest again + # (note: we're not revoked yet because that triggers a break above) + replaced_ts = None + latest_ts = entry['revoke_ts'] + if last_latest is None: + timestamps = [] + else: + timestamps = [last_latest] + if revoke_ts is None: + if replaced_ts is None: + # turns out we are still latest + is_latest = True + if options.debug: + print("%s is latest (again) in tag %s" % (nvr, tag_name)) + break + else: + # replaced (but not revoked) + timestamps.append(replaced_ts) + if options.debug: + print("tag %s: %s not latest (replaced %s)" + % (tag_name, nvr, time.asctime(time.localtime(replaced_ts)))) + elif replaced_ts is None: + # revoked but not replaced + timestamps.append(revoke_ts) + if options.debug: + print("tag %s: %s not latest (revoked %s)" + % (tag_name, nvr, time.asctime(time.localtime(revoke_ts)))) + else: + # revoked AND replaced + timestamps.append(min(revoke_ts, replaced_ts)) + if options.debug: + print("tag %s: %s not latest (revoked %s, replaced %s)" + % (tag_name, nvr, time.asctime(time.localtime(revoke_ts)), + time.asctime(time.localtime(replaced_ts)))) + last_latest = max(timestamps) + if last_latest > cutoff_ts: + if options.debug: + print("%s was latest past the cutoff" % nvr) + is_latest = True + break + if is_latest: + continue + if is_protected: + continue + # not latest anywhere since cutoff, so we can remove all signed copies + rpms = session.listRPMs(buildID=binfo['id']) + session.multicall = True + for rpminfo in rpms: + session.queryRPMSigs(rpm_id=rpminfo['id']) + by_sig = {} + # index by sig + for rpminfo, [sigs] in zip(rpms, session.multiCall()): + for sig in sigs: + sigkey = sig['sigkey'] + by_sig.setdefault(sigkey, []).append(rpminfo) + builddir = koji.pathinfo.build(binfo) + build_files = 0 + build_space = 0 + if not by_sig and options.debug: + print("(build has no signatures)") + for sigkey, rpms in six.iteritems(by_sig): + mycount = 0 + archdirs = {} + sigdirs = {} + for rpminfo in rpms: + signedpath = "%s/%s" % (builddir, koji.pathinfo.signed(rpminfo, sigkey)) + try: + st = os.lstat(signedpath) + except OSError: + continue + if not stat.S_ISREG(st.st_mode): + # warn about this + print("Skipping %s. Not a regular file" % signedpath) + continue + if st.st_mtime > cutoff_ts: + print("Skipping %s. File newer than cutoff" % signedpath) + continue + if options.test: + print("Would have unlinked: %s" % signedpath) + else: + if options.verbose: + print("Unlinking: %s" % signedpath) + try: + os.unlink(signedpath) + except OSError as e: + print("Error removing %s: %s" % (signedpath, e)) + print("This script needs write access to %s" % koji.BASEDIR) + continue + mycount += 1 + build_files += 1 + build_space += st.st_size + # XXX - this makes some layout assumptions, but + # pathinfo doesn't report what we need + mydir = os.path.dirname(signedpath) + archdirs[mydir] = 1 + sigdirs[os.path.dirname(mydir)] = 1 + for dir in archdirs: + if options.test: + print("Would have removed dir: %s" % dir) + else: + if options.verbose: + print("Removing dir: %s" % dir) + try: + os.rmdir(dir) + except OSError as e: + print("Error removing %s: %s" % (signedpath, e)) + if len(sigdirs) == 1: + dir = to_list(sigdirs.keys())[0] + if options.test: + print("Would have removed dir: %s" % dir) + else: + if options.verbose: + print("Removing dir: %s" % dir) + try: + os.rmdir(dir) + except OSError as e: + print("Error removing %s: %s" % (signedpath, e)) + elif len(sigdirs) > 1: + warn("More than one signature dir for %s: %r" % (sigkey, sigdirs)) + if build_files: + total_files += build_files + total_space += build_space + if options.verbose: + print("Build: %s, Removed %i signed copies (%i bytes). Total: %i/%i" + % (nvr, build_files, build_space, total_files, total_space)) + elif options.debug and by_sig: + print("(build has no signed copies)") + print("--- Grand Totals ---") + print("Files: %i" % total_files) + print("Bytes: %i" % total_space) diff --git a/cli/koji_cli/commands/regen_repo.py b/cli/koji_cli/commands/regen_repo.py new file mode 100644 index 0000000..41dd2b7 --- /dev/null +++ b/cli/koji_cli/commands/regen_repo.py @@ -0,0 +1,70 @@ +from __future__ import absolute_import, division + +from optparse import OptionParser + + +from koji_cli.lib import ( + _running_in_bg, + activate_session, + get_usage_str, + warn, + watch_tasks +) + + +def handle_regen_repo(options, session, args): + "[admin] Force a repo to be regenerated" + usage = "usage: %prog regen-repo [options] " + parser = OptionParser(usage=get_usage_str(usage)) + parser.add_option("--target", action="store_true", + help="Interpret the argument as a build target name") + parser.add_option("--wait", action="store_true", + help="Wait on for regen to finish, even if running in the background") + parser.add_option("--nowait", action="store_false", dest="wait", + help="Don't wait on for regen to finish") + parser.add_option("--debuginfo", action="store_true", help="Include debuginfo rpms in repo") + parser.add_option("--source", "--src", action="store_true", + help="Include source rpms in each of repos") + parser.add_option("--separate-source", "--separate-src", action="store_true", + help="Include source rpms in separate src repo") + (suboptions, args) = parser.parse_args(args) + if len(args) == 0: + parser.error("A tag name must be specified") + elif len(args) > 1: + if suboptions.target: + parser.error("Only a single target may be specified") + else: + parser.error("Only a single tag name may be specified") + activate_session(session, options) + tag = args[0] + repo_opts = {} + if suboptions.target: + info = session.getBuildTarget(tag) + if not info: + parser.error("No such build target: %s" % tag) + tag = info['build_tag_name'] + info = session.getTag(tag, strict=True) + else: + info = session.getTag(tag) + if not info: + parser.error("No such tag: %s" % tag) + tag = info['name'] + targets = session.getBuildTargets(buildTagID=info['id']) + if not targets: + warn("%s is not a build tag" % tag) + if not info['arches']: + warn("Tag %s has an empty arch list" % info['name']) + if suboptions.debuginfo: + repo_opts['debuginfo'] = True + if suboptions.source: + repo_opts['src'] = True + if suboptions.separate_source: + repo_opts['separate_src'] = True + task_id = session.newRepo(tag, **repo_opts) + print("Regenerating repo for tag: %s" % tag) + print("Created task: %d" % task_id) + print("Task info: %s/taskinfo?taskID=%s" % (options.weburl, task_id)) + if suboptions.wait or (suboptions.wait is None and not _running_in_bg()): + session.logout() + return watch_tasks(session, [task_id], quiet=options.quiet, + poll_interval=options.poll_interval, topurl=options.topurl) diff --git a/cli/koji_cli/commands/remove_channel.py b/cli/koji_cli/commands/remove_channel.py new file mode 100644 index 0000000..c1477bb --- /dev/null +++ b/cli/koji_cli/commands/remove_channel.py @@ -0,0 +1,26 @@ +from __future__ import absolute_import, division + +from optparse import OptionParser + + +from koji_cli.lib import ( + activate_session, + error, + get_usage_str +) + + +def handle_remove_channel(goptions, session, args): + "[admin] Remove a channel entirely" + usage = "usage: %prog remove-channel [options] " + parser = OptionParser(usage=get_usage_str(usage)) + parser.add_option("--force", action="store_true", help="force removal, if possible") + (options, args) = parser.parse_args(args) + print("remove-channel is deprecated and will be removed in 1.30") + if len(args) != 1: + parser.error("Incorrect number of arguments") + activate_session(session, goptions) + cinfo = session.getChannel(args[0]) + if not cinfo: + error("No such channel: %s" % args[0]) + session.removeChannel(args[0], force=options.force) diff --git a/cli/koji_cli/commands/remove_external_repo.py b/cli/koji_cli/commands/remove_external_repo.py new file mode 100644 index 0000000..eccaae3 --- /dev/null +++ b/cli/koji_cli/commands/remove_external_repo.py @@ -0,0 +1,51 @@ +from __future__ import absolute_import, division + +from optparse import OptionParser + + +from koji_cli.lib import ( + activate_session, + error, + get_usage_str, + warn +) + + +def handle_remove_external_repo(goptions, session, args): + "[admin] Remove an external repo from a tag or tags, or remove entirely" + usage = "usage: %prog remove-external-repo [ ...]" + parser = OptionParser(usage=get_usage_str(usage)) + parser.add_option("--alltags", action="store_true", help="Remove from all tags") + parser.add_option("--force", action='store_true', help="Force action") + (options, args) = parser.parse_args(args) + if len(args) < 1: + parser.error("Incorrect number of arguments") + activate_session(session, goptions) + repo = args[0] + tags = args[1:] + delete = not bool(tags) + data = session.getTagExternalRepos(repo_info=repo) + current_tags = [d['tag_name'] for d in data] + if options.alltags: + delete = False + if tags: + parser.error("Do not specify tags when using --alltags") + if not current_tags: + if options.force: + delete = True + else: + warn("External repo %s not associated with any tags" % repo) + return 0 + tags = current_tags + if delete: + # removing entirely + if current_tags and not options.force: + warn("Error: external repo %s used by tag(s): %s" % (repo, ', '.join(current_tags))) + error("Use --force to remove anyway") + session.deleteExternalRepo(args[0]) + else: + for tag in tags: + if tag not in current_tags: + print("External repo %s not associated with tag %s" % (repo, tag)) + continue + session.removeExternalRepoFromTag(tag, repo) diff --git a/cli/koji_cli/commands/remove_group.py b/cli/koji_cli/commands/remove_group.py new file mode 100644 index 0000000..cd259df --- /dev/null +++ b/cli/koji_cli/commands/remove_group.py @@ -0,0 +1,36 @@ +from __future__ import absolute_import, division + +from optparse import OptionParser + + +from koji_cli.lib import ( + activate_session, + error, + get_usage_str +) + + +def handle_remove_group(goptions, session, args): + "[admin] Remove group from tag" + usage = "usage: %prog remove-group " + parser = OptionParser(usage=get_usage_str(usage)) + (options, args) = parser.parse_args(args) + if len(args) != 2: + parser.error("Please specify a tag name and a group name") + tag = args[0] + group = args[1] + + activate_session(session, goptions) + if not (session.hasPerm('admin') or session.hasPerm('tag')): + parser.error("This action requires tag or admin privileges") + + dsttag = session.getTag(tag) + if not dsttag: + error("No such tag: %s" % tag) + + groups = dict([(p['name'], p['group_id']) for p in session.getTagGroups(tag, inherit=False)]) + group_id = groups.get(group, None) + if group_id is None: + error("Group %s doesn't exist within tag %s" % (group, tag)) + + session.groupListRemove(tag, group) diff --git a/cli/koji_cli/commands/remove_host_from_channel.py b/cli/koji_cli/commands/remove_host_from_channel.py new file mode 100644 index 0000000..f68fa8d --- /dev/null +++ b/cli/koji_cli/commands/remove_host_from_channel.py @@ -0,0 +1,31 @@ +from __future__ import absolute_import, division + +from optparse import OptionParser + + +from koji_cli.lib import ( + activate_session, + error, + get_usage_str +) + + +def handle_remove_host_from_channel(goptions, session, args): + "[admin] Remove a host from a channel" + usage = "usage: %prog remove-host-from-channel [options] " + parser = OptionParser(usage=get_usage_str(usage)) + (options, args) = parser.parse_args(args) + if len(args) != 2: + parser.error("Please specify a hostname and a channel") + host = args[0] + activate_session(session, goptions) + hostinfo = session.getHost(host) + if not hostinfo: + error("No such host: %s" % host) + hostchannels = [c['name'] for c in session.listChannels(hostinfo['id'])] + + channel = args[1] + if channel not in hostchannels: + error("Host %s is not a member of channel %s" % (host, channel)) + + session.removeHostFromChannel(host, channel) diff --git a/cli/koji_cli/commands/remove_notification.py b/cli/koji_cli/commands/remove_notification.py new file mode 100644 index 0000000..a1af5a1 --- /dev/null +++ b/cli/koji_cli/commands/remove_notification.py @@ -0,0 +1,31 @@ +from __future__ import absolute_import, division + +from optparse import OptionParser + + +from koji_cli.lib import ( + activate_session, + get_usage_str +) + + +def handle_remove_notification(goptions, session, args): + "[monitor] Remove user's notifications" + usage = "usage: %prog remove-notification [options] [ ...]" + parser = OptionParser(usage=get_usage_str(usage)) + (options, args) = parser.parse_args(args) + + activate_session(session, goptions) + + if len(args) < 1: + parser.error("At least one notification id has to be specified") + + try: + n_ids = [int(x) for x in args] + except ValueError: + parser.error("All notification ids has to be integers") + + for n_id in n_ids: + session.deleteNotification(n_id) + if not goptions.quiet: + print("Notification %d successfully removed." % n_id) diff --git a/cli/koji_cli/commands/remove_pkg.py b/cli/koji_cli/commands/remove_pkg.py new file mode 100644 index 0000000..ca48f45 --- /dev/null +++ b/cli/koji_cli/commands/remove_pkg.py @@ -0,0 +1,48 @@ +from __future__ import absolute_import, division + +from optparse import OptionParser + +import koji + +from koji_cli.lib import ( + activate_session, + error, + get_usage_str, + warn +) + + +def handle_remove_pkg(goptions, session, args): + "[admin] Remove a package from the listing for tag" + usage = "usage: %prog remove-pkg [options] [ ...]" + parser = OptionParser(usage=get_usage_str(usage)) + parser.add_option("--force", action='store_true', help="Override blocks if necessary") + (options, args) = parser.parse_args(args) + if len(args) < 2: + parser.error("Please specify a tag and at least one package") + activate_session(session, goptions) + tag = args[0] + opts = {} + opts['force'] = options.force + # check if list of packages exists for that tag already + dsttag = session.getTag(tag) + if dsttag is None: + error("No such tag: %s" % tag) + try: + pkglist = session.listPackages(tagID=dsttag['id'], with_owners=False) + except koji.ParameterError: + # performance option added in 1.25 + pkglist = session.listPackages(tagID=dsttag['id']) + pkglist = dict([(p['package_name'], p['package_id']) for p in pkglist]) + ret = 0 + for package in args[1:]: + package_id = pkglist.get(package, None) + if package_id is None: + warn("Package %s is not in tag %s" % (package, tag)) + ret = 1 + if ret: + error(code=ret) + session.multicall = True + for package in args[1:]: + session.packageListRemove(tag, package, **opts) + session.multiCall(strict=True) diff --git a/cli/koji_cli/commands/remove_sig.py b/cli/koji_cli/commands/remove_sig.py new file mode 100644 index 0000000..9f8a859 --- /dev/null +++ b/cli/koji_cli/commands/remove_sig.py @@ -0,0 +1,44 @@ +from __future__ import absolute_import, division + +from optparse import OptionParser + +import koji + +from koji_cli.lib import ( + activate_session, + error, + get_usage_str +) + + +def handle_remove_sig(goptions, session, args): + "[admin] Remove signed RPMs from db and disk" + usage = "usage: %prog remove-sig [options] " + description = "Only use this method in extreme situations, because it " + description += "goes against Koji's design of immutable, auditable data." + parser = OptionParser(usage=get_usage_str(usage), description=description) + parser.add_option("--sigkey", action="store", default=None, help="Specify signature key") + parser.add_option("--all", action="store_true", default=False, + help="Remove all signed copies for specified RPM") + (options, args) = parser.parse_args(args) + if len(args) < 1: + parser.error("Please specify an RPM") + + if not options.all and not options.sigkey: + error("Either --sigkey or --all options must be given") + + if options.all and options.sigkey: + error("Conflicting options specified") + + activate_session(session, goptions) + rpminfo = args[0] + + try: + session.deleteRPMSig(rpminfo, sigkey=options.sigkey, all_sigs=options.all) + except koji.GenericError as e: + msg = str(e) + if msg.startswith("No such rpm"): + # make this a little more readable than the hub error + error("No such rpm in system: %s" % rpminfo) + else: + error("Signature removal failed: %s" % msg) diff --git a/cli/koji_cli/commands/remove_tag.py b/cli/koji_cli/commands/remove_tag.py new file mode 100644 index 0000000..4f42509 --- /dev/null +++ b/cli/koji_cli/commands/remove_tag.py @@ -0,0 +1,31 @@ +from __future__ import absolute_import, division + +from optparse import OptionParser + + +from koji_cli.lib import ( + activate_session, + error, + get_usage_str +) + + +def handle_remove_tag(goptions, session, args): + "[admin] Remove a tag" + usage = "usage: %prog remove-tag [options] " + parser = OptionParser(usage=get_usage_str(usage)) + (options, args) = parser.parse_args(args) + + if len(args) != 1: + parser.error("Please specify a tag to remove") + activate_session(session, goptions) + + if not (session.hasPerm('admin') or session.hasPerm('tag')): + parser.error("This action requires tag or admin privileges") + + tag = args[0] + tag_info = session.getTag(tag) + if not tag_info: + error("No such tag: %s" % tag) + + session.deleteTag(tag_info['id']) diff --git a/cli/koji_cli/commands/remove_tag_inheritance.py b/cli/koji_cli/commands/remove_tag_inheritance.py new file mode 100644 index 0000000..634fc2b --- /dev/null +++ b/cli/koji_cli/commands/remove_tag_inheritance.py @@ -0,0 +1,68 @@ +from __future__ import absolute_import, division + +from optparse import OptionParser + + +from koji_cli.lib import ( + activate_session, + error, + get_usage_str +) + + +def handle_remove_tag_inheritance(goptions, session, args): + """[admin] Remove a tag inheritance link""" + usage = "usage: %prog remove-tag-inheritance " + parser = OptionParser(usage=get_usage_str(usage)) + (options, args) = parser.parse_args(args) + + if len(args) < 1: + parser.error("This command takes at least one argument: a tag name or ID") + + if len(args) > 3: + parser.error("This command takes at most three argument: a tag name or ID, a parent tag " + "name or ID, and a priority") + + activate_session(session, goptions) + + tag = session.getTag(args[0]) + if not tag: + parser.error("No such tag: %s" % args[0]) + + parent = None + priority = None + if len(args) > 1: + parent = session.getTag(args[1]) + if not parent: + parser.error("No such tag: %s" % args[1]) + if len(args) > 2: + priority = args[2] + + data = session.getInheritanceData(tag['id']) + if parent and data: + data = [datum for datum in data if datum['parent_id'] == parent['id']] + if priority and data: + data = [datum for datum in data if datum['priority'] == priority] + + if len(data) == 0: + error("No inheritance link found to remove. Please check your arguments") + elif len(data) > 1: + print("Multiple matches for tag.") + if not parent: + error("Please specify a parent on the command line.") + if not priority: + error("Please specify a priority on the command line.") + error("Error: Key constraints may be broken. Exiting.") + + # len(data) == 1 + data = data[0] + + inheritanceData = session.getInheritanceData(tag['id']) + + new_data = data.copy() + new_data['delete link'] = True + + # find the data we want to edit and replace it + index = inheritanceData.index(data) + inheritanceData[index] = new_data + session.setInheritanceData(tag['id'], inheritanceData) diff --git a/cli/koji_cli/commands/remove_target.py b/cli/koji_cli/commands/remove_target.py new file mode 100644 index 0000000..0d79f86 --- /dev/null +++ b/cli/koji_cli/commands/remove_target.py @@ -0,0 +1,31 @@ +from __future__ import absolute_import, division + +from optparse import OptionParser + + +from koji_cli.lib import ( + activate_session, + error, + get_usage_str +) + + +def handle_remove_target(goptions, session, args): + "[admin] Remove a build target" + usage = "usage: %prog remove-target [options] " + parser = OptionParser(usage=get_usage_str(usage)) + (options, args) = parser.parse_args(args) + + if len(args) != 1: + parser.error("Please specify a build target to remove") + activate_session(session, goptions) + + if not (session.hasPerm('admin') or session.hasPerm('target')): + parser.error("This action requires target or admin privileges") + + target = args[0] + target_info = session.getBuildTarget(target) + if not target_info: + error("No such build target: %s" % target) + + session.deleteBuildTarget(target_info['id']) diff --git a/cli/koji_cli/commands/restart_hosts.py b/cli/koji_cli/commands/restart_hosts.py new file mode 100644 index 0000000..dd2995d --- /dev/null +++ b/cli/koji_cli/commands/restart_hosts.py @@ -0,0 +1,66 @@ +from __future__ import absolute_import, division + +from optparse import OptionParser + +import koji + +from koji_cli.lib import ( + _running_in_bg, + activate_session, + error, + get_usage_str, + warn, + watch_tasks +) + + +def handle_restart_hosts(options, session, args): + "[admin] Restart enabled hosts" + usage = "usage: %prog restart-hosts [options]" + parser = OptionParser(usage=get_usage_str(usage)) + parser.add_option("--wait", action="store_true", + help="Wait on the task, even if running in the background") + parser.add_option("--nowait", action="store_false", dest="wait", help="Don't wait on task") + parser.add_option("--quiet", action="store_true", + help="Do not print the task information", default=options.quiet) + parser.add_option("--force", action="store_true", help="Ignore checks and force operation") + parser.add_option("--channel", help="Only hosts in this channel") + parser.add_option("--arch", "-a", action="append", default=[], + help="Limit to hosts of this architecture (can be given multiple times)") + parser.add_option("--timeout", metavar='N', type='int', help="Time out after N seconds") + (my_opts, args) = parser.parse_args(args) + + if len(args) > 0: + parser.error("restart-hosts does not accept arguments") + + activate_session(session, options) + + # check for existing restart tasks + if not my_opts.force: + query = { + 'method': 'restartHosts', + 'state': + [koji.TASK_STATES[s] for s in ('FREE', 'OPEN', 'ASSIGNED')], + } + others = session.listTasks(query) + if others: + warn('Found other restartHosts tasks running.') + warn('Task ids: %r' % [t['id'] for t in others]) + error('Use --force to run anyway') + + callopts = {} + if my_opts.channel: + callopts['channel'] = my_opts.channel + if my_opts.arch: + callopts['arches'] = my_opts.arch + if my_opts.timeout: + callopts['timeout'] = my_opts.timeout + if callopts: + task_id = session.restartHosts(options=callopts) + else: + # allow default case to work with older hub + task_id = session.restartHosts() + if my_opts.wait or (my_opts.wait is None and not _running_in_bg()): + session.logout() + return watch_tasks(session, [task_id], quiet=my_opts.quiet, + poll_interval=options.poll_interval, topurl=options.topurl) diff --git a/cli/koji_cli/commands/resubmit.py b/cli/koji_cli/commands/resubmit.py new file mode 100644 index 0000000..e2fe014 --- /dev/null +++ b/cli/koji_cli/commands/resubmit.py @@ -0,0 +1,40 @@ +from __future__ import absolute_import, division + +from optparse import OptionParser + + +from koji_cli.lib import ( + _running_in_bg, + activate_session, + get_usage_str, + printTaskInfo, + watch_tasks, +) + + +def handle_resubmit(goptions, session, args): + """[build] Retry a canceled or failed task, using the same parameter as the original task.""" + usage = "usage: %prog resubmit [options] " + parser = OptionParser(usage=get_usage_str(usage)) + parser.add_option("--wait", action="store_true", + help="Wait on task, even if running in the background") + parser.add_option("--nowait", action="store_false", dest="wait", help="Don't wait on task") + parser.add_option("--nowatch", action="store_true", dest="nowait", + help="An alias for --nowait") + parser.add_option("--quiet", action="store_true", default=goptions.quiet, + help="Do not print the task information") + (options, args) = parser.parse_args(args) + if len(args) != 1: + parser.error("Please specify a single task ID") + activate_session(session, goptions) + taskID = int(args[0]) + if not options.quiet: + print("Resubmitting the following task:") + printTaskInfo(session, taskID, goptions.topdir, 0, False, True) + newID = session.resubmitTask(taskID) + if not options.quiet: + print("Resubmitted task %s as new task %s" % (taskID, newID)) + if options.wait or (options.wait is None and not _running_in_bg()): + session.logout() + return watch_tasks(session, [newID], quiet=options.quiet, + poll_interval=goptions.poll_interval, topurl=goptions.topurl) diff --git a/cli/koji_cli/commands/revoke_cg_access.py b/cli/koji_cli/commands/revoke_cg_access.py new file mode 100644 index 0000000..b461af1 --- /dev/null +++ b/cli/koji_cli/commands/revoke_cg_access.py @@ -0,0 +1,25 @@ +from __future__ import absolute_import, division + +from optparse import OptionParser + + +from koji_cli.lib import ( + activate_session, + get_usage_str +) + + +def handle_revoke_cg_access(goptions, session, args): + "[admin] Remove a user from a content generator" + usage = "usage: %prog revoke-cg-access " + parser = OptionParser(usage=get_usage_str(usage)) + (options, args) = parser.parse_args(args) + if len(args) != 2: + parser.error("Please specify a user and content generator") + activate_session(session, goptions) + user = args[0] + cg = args[1] + uinfo = session.getUser(user) + if uinfo is None: + parser.error("No such user: %s" % user) + session.revokeCGAccess(uinfo['name'], cg) diff --git a/cli/koji_cli/commands/revoke_permission.py b/cli/koji_cli/commands/revoke_permission.py new file mode 100644 index 0000000..edc8b34 --- /dev/null +++ b/cli/koji_cli/commands/revoke_permission.py @@ -0,0 +1,29 @@ +from __future__ import absolute_import, division + +from optparse import OptionParser + + +from koji_cli.lib import ( + activate_session, + get_usage_str +) + + +def handle_revoke_permission(goptions, session, args): + "[admin] Revoke a permission from a user" + usage = "usage: %prog revoke-permission [ ...]" + parser = OptionParser(usage=get_usage_str(usage)) + (options, args) = parser.parse_args(args) + if len(args) < 2: + parser.error("Please specify a permission and at least one user") + activate_session(session, goptions) + perm = args[0] + names = args[1:] + users = [] + for n in names: + user = session.getUser(n) + if user is None: + parser.error("No such user: %s" % n) + users.append(user) + for user in users: + session.revokePermission(user['name'], perm) diff --git a/cli/koji_cli/commands/rpminfo.py b/cli/koji_cli/commands/rpminfo.py new file mode 100644 index 0000000..9ea4ad5 --- /dev/null +++ b/cli/koji_cli/commands/rpminfo.py @@ -0,0 +1,91 @@ +from __future__ import absolute_import, division + +import os +import time +from optparse import OptionParser + +import koji + +from koji_cli.lib import ( + ensure_connection, + error, + get_usage_str, + warn +) + + +def anon_handle_rpminfo(goptions, session, args): + "[info] Print basic information about an RPM" + usage = "usage: %prog rpminfo [options] [ ...]" + parser = OptionParser(usage=get_usage_str(usage)) + parser.add_option("--buildroots", action="store_true", + help="show buildroots the rpm was used in") + (options, args) = parser.parse_args(args) + if len(args) < 1: + parser.error("Please specify an RPM") + ensure_connection(session, goptions) + error_hit = False + for rpm in args: + info = session.getRPM(rpm) + if info is None: + warn("No such rpm: %s\n" % rpm) + error_hit = True + continue + if info['epoch'] is None: + info['epoch'] = "" + else: + info['epoch'] = str(info['epoch']) + ":" + if not info.get('external_repo_id', 0): + buildinfo = session.getBuild(info['build_id']) + buildinfo['name'] = buildinfo['package_name'] + buildinfo['arch'] = 'src' + if buildinfo['epoch'] is None: + buildinfo['epoch'] = "" + else: + buildinfo['epoch'] = str(buildinfo['epoch']) + ":" + print("RPM: %(epoch)s%(name)s-%(version)s-%(release)s.%(arch)s [%(id)d]" % info) + if info.get('external_repo_id'): + repo = session.getExternalRepo(info['external_repo_id']) + print("External Repository: %(name)s [%(id)i]" % repo) + print("External Repository url: %(url)s" % repo) + else: + print("RPM Path: %s" % + os.path.join(koji.pathinfo.build(buildinfo), koji.pathinfo.rpm(info))) + print("SRPM: %(epoch)s%(name)s-%(version)s-%(release)s [%(id)d]" % buildinfo) + print("SRPM Path: %s" % + os.path.join(koji.pathinfo.build(buildinfo), koji.pathinfo.rpm(buildinfo))) + print("Built: %s" % time.strftime('%a, %d %b %Y %H:%M:%S %Z', + time.localtime(info['buildtime']))) + print("SIGMD5: %(payloadhash)s" % info) + print("Size: %(size)s" % info) + if not info.get('external_repo_id', 0): + headers = session.getRPMHeaders(rpmID=info['id'], + headers=["license"]) + if 'license' in headers: + print("License: %(license)s" % headers) + print("Build ID: %(build_id)s" % info) + if info['buildroot_id'] is None: + print("No buildroot data available") + else: + br_info = session.getBuildroot(info['buildroot_id']) + if br_info['br_type'] == koji.BR_TYPES['STANDARD']: + print("Buildroot: %(id)i (tag %(tag_name)s, arch %(arch)s, repo %(repo_id)i)" % + br_info) + print("Build Host: %(host_name)s" % br_info) + print("Build Task: %(task_id)i" % br_info) + else: + print("Content generator: %(cg_name)s" % br_info) + print("Buildroot: %(id)i" % br_info) + print("Build Host OS: %(host_os)s (%(host_arch)s)" % br_info) + if info.get('extra'): + print("Extra: %(extra)r" % info) + if options.buildroots: + br_list = session.listBuildroots(rpmID=info['id'], queryOpts={'order': 'buildroot.id'}) + print("Used in %i buildroots:" % len(br_list)) + if len(br_list): + print(" %8s %-28s %-8s %-29s" % ('id', 'build tag', 'arch', 'build host')) + print(" %s %s %s %s" % ('-' * 8, '-' * 28, '-' * 8, '-' * 29)) + for br_info in br_list: + print(" %(id)8i %(tag_name)-28s %(arch)-8s %(host_name)-29s" % br_info) + if error_hit: + error() diff --git a/cli/koji_cli/commands/search.py b/cli/koji_cli/commands/search.py new file mode 100644 index 0000000..b7d5dcd --- /dev/null +++ b/cli/koji_cli/commands/search.py @@ -0,0 +1,39 @@ +from __future__ import absolute_import, division + +from optparse import OptionParser + + +from koji_cli.lib import ( + ensure_connection, + get_usage_str +) + +_search_types = ('package', 'build', 'tag', 'target', 'user', 'host', 'rpm', + 'maven', 'win') + + +def anon_handle_search(goptions, session, args): + "[search] Search the system" + usage = "usage: %prog search [options] " + usage += '\nAvailable search types: %s' % ', '.join(_search_types) + parser = OptionParser(usage=get_usage_str(usage)) + parser.add_option("-r", "--regex", action="store_true", help="treat pattern as regex") + parser.add_option("--exact", action="store_true", help="exact matches only") + (options, args) = parser.parse_args(args) + if len(args) < 1: + parser.error("Please specify search type") + if len(args) < 2: + parser.error("Please specify search pattern") + type = args[0] + if type not in _search_types: + parser.error("No such search type: %s" % type) + pattern = args[1] + matchType = 'glob' + if options.regex: + matchType = 'regexp' + elif options.exact: + matchType = 'exact' + ensure_connection(session, goptions) + data = session.search(pattern, type, matchType) + for row in data: + print(row['name']) diff --git a/cli/koji_cli/commands/set_build_volume.py b/cli/koji_cli/commands/set_build_volume.py new file mode 100644 index 0000000..1ea5870 --- /dev/null +++ b/cli/koji_cli/commands/set_build_volume.py @@ -0,0 +1,39 @@ +from __future__ import absolute_import, division + +from optparse import OptionParser + + +from koji_cli.lib import ( + activate_session, + error, + get_usage_str +) + + +def handle_set_build_volume(goptions, session, args): + "[admin] Move a build to a different volume" + usage = "usage: %prog set-build-volume [ ...]" + parser = OptionParser(usage=get_usage_str(usage)) + parser.add_option("-v", "--verbose", action="store_true", help="Be verbose") + (options, args) = parser.parse_args(args) + if len(args) < 2: + parser.error("You must provide a volume and at least one build") + volinfo = session.getVolume(args[0]) + if not volinfo: + error("No such volume: %s" % args[0]) + activate_session(session, goptions) + builds = [] + for nvr in args[1:]: + binfo = session.getBuild(nvr) + if not binfo: + print("No such build: %s" % nvr) + elif binfo['volume_id'] == volinfo['id']: + print("Build %s already on volume %s" % (nvr, volinfo['name'])) + else: + builds.append(binfo) + if not builds: + error("No builds to move") + for binfo in builds: + session.changeBuildVolume(binfo['id'], volinfo['id']) + if options.verbose: + print("%s: %s -> %s" % (binfo['nvr'], binfo['volume_name'], volinfo['name'])) diff --git a/cli/koji_cli/commands/set_pkg_arches.py b/cli/koji_cli/commands/set_pkg_arches.py new file mode 100644 index 0000000..1bb80fd --- /dev/null +++ b/cli/koji_cli/commands/set_pkg_arches.py @@ -0,0 +1,26 @@ +from __future__ import absolute_import, division + +from optparse import OptionParser + +import koji + +from koji_cli.lib import ( + activate_session, + get_usage_str +) + + +def handle_set_pkg_arches(goptions, session, args): + "[admin] Set the list of extra arches for a package" + usage = "usage: %prog set-pkg-arches [options] [ ...]" + parser = OptionParser(usage=get_usage_str(usage)) + parser.add_option("--force", action='store_true', help="Force operation") + (options, args) = parser.parse_args(args) + if len(args) < 3: + parser.error("Please specify an archlist, a tag, and at least one package") + activate_session(session, goptions) + arches = koji.parse_arches(args[0]) + tag = args[1] + with session.multicall(strict=True) as m: + for package in args[2:]: + m.packageListSetArches(tag, package, arches, force=options.force) diff --git a/cli/koji_cli/commands/set_pkg_owner.py b/cli/koji_cli/commands/set_pkg_owner.py new file mode 100644 index 0000000..c5a6918 --- /dev/null +++ b/cli/koji_cli/commands/set_pkg_owner.py @@ -0,0 +1,25 @@ +from __future__ import absolute_import, division + +from optparse import OptionParser + + +from koji_cli.lib import ( + activate_session, + get_usage_str +) + + +def handle_set_pkg_owner(goptions, session, args): + "[admin] Set the owner for a package" + usage = "usage: %prog set-pkg-owner [options] [ ...]" + parser = OptionParser(usage=get_usage_str(usage)) + parser.add_option("--force", action='store_true', help="Force operation") + (options, args) = parser.parse_args(args) + if len(args) < 3: + parser.error("Please specify an owner, a tag, and at least one package") + activate_session(session, goptions) + owner = args[0] + tag = args[1] + with session.multicall(strict=True) as m: + for package in args[2:]: + m.packageListSetOwner(tag, package, owner, force=options.force) diff --git a/cli/koji_cli/commands/set_pkg_owner_global.py b/cli/koji_cli/commands/set_pkg_owner_global.py new file mode 100644 index 0000000..ab81068 --- /dev/null +++ b/cli/koji_cli/commands/set_pkg_owner_global.py @@ -0,0 +1,67 @@ +from __future__ import absolute_import, division + +from optparse import OptionParser + + +from koji_cli.lib import ( + activate_session, + error, + get_usage_str +) + + +def handle_set_pkg_owner_global(goptions, session, args): + "[admin] Set the owner for a package globally" + usage = "usage: %prog set-pkg-owner-global [options] [ ...]" + parser = OptionParser(usage=get_usage_str(usage)) + parser.add_option("--verbose", action='store_true', help="List changes") + parser.add_option("--test", action='store_true', help="Test mode") + parser.add_option("--old-user", "--from", action="store", + help="Only change ownership for packages belonging to this user") + (options, args) = parser.parse_args(args) + if options.old_user: + if len(args) < 1: + parser.error("Please specify an owner") + elif len(args) < 2: + parser.error("Please specify an owner and at least one package") + activate_session(session, goptions) + owner = args[0] + packages = args[1:] + user = session.getUser(owner) + if not user: + error("No such user: %s" % owner) + opts = {'with_dups': True} + old_user = None + if options.old_user: + old_user = session.getUser(options.old_user) + if not old_user: + error("No such user: %s" % options.old_user) + opts['userID'] = old_user['id'] + to_change = [] + for package in packages: + entries = session.listPackages(pkgID=package, **opts) + if not entries: + print("No data for package %s" % package) + continue + to_change.extend(entries) + if not packages and options.old_user: + entries = session.listPackages(**opts) + if not entries: + error("No data for user %s" % old_user['name']) + to_change.extend(entries) + for entry in to_change: + if user['id'] == entry['owner_id']: + if options.verbose: + print("Preserving owner=%s for package %s in tag %s" + % (user['name'], package, entry['tag_name'])) + else: + if options.test: + print("Would have changed owner for %s in tag %s: %s -> %s" + % (entry['package_name'], entry['tag_name'], entry['owner_name'], + user['name'])) + continue + if options.verbose: + print("Changing owner for %s in tag %s: %s -> %s" + % (entry['package_name'], entry['tag_name'], entry['owner_name'], + user['name'])) + session.packageListSetOwner(entry['tag_id'], entry['package_name'], user['id']) diff --git a/cli/koji_cli/commands/set_task_priority.py b/cli/koji_cli/commands/set_task_priority.py new file mode 100644 index 0000000..ec00fb7 --- /dev/null +++ b/cli/koji_cli/commands/set_task_priority.py @@ -0,0 +1,44 @@ +from __future__ import absolute_import, division + +from optparse import OptionParser + +import koji + +from koji_cli.lib import ( + activate_session, + error, + get_usage_str, + warn +) + + +def handle_set_task_priority(goptions, session, args): + "[admin] Set task priority" + usage = "usage: %prog set-task-priority [options] --priority= " \ + "[ ...]" + parser = OptionParser(usage=get_usage_str(usage)) + parser.add_option("--priority", type="int", help="New priority") + parser.add_option("--recurse", action="store_true", default=False, + help="Change priority of child tasks as well") + (options, args) = parser.parse_args(args) + if len(args) == 0: + parser.error("You must specify at least one task id") + + if options.priority is None: + parser.error("You must specify --priority") + try: + tasks = [int(a) for a in args] + except ValueError: + parser.error("Task numbers must be integers") + + activate_session(session, goptions) + + if not session.hasPerm('admin'): + logged_user = session.getLoggedInUser() + error("admin permission required (logged in as %s)" % logged_user['name']) + + for task_id in tasks: + try: + session.setTaskPriority(task_id, options.priority, options.recurse) + except koji.GenericError: + warn("Can't update task priority on closed task: %s" % task_id) diff --git a/cli/koji_cli/commands/show_groups.py b/cli/koji_cli/commands/show_groups.py new file mode 100644 index 0000000..c9de5c0 --- /dev/null +++ b/cli/koji_cli/commands/show_groups.py @@ -0,0 +1,40 @@ +from __future__ import absolute_import, division + +import pprint +from optparse import OptionParser + +import koji + +from koji_cli.lib import ( + ensure_connection, + get_usage_str +) + + +def anon_handle_show_groups(goptions, session, args): + "[info] Show groups data for a tag" + usage = "usage: %prog show-groups [options] " + parser = OptionParser(usage=get_usage_str(usage)) + parser.add_option("--comps", action="store_true", help="Print in comps format") + parser.add_option("-x", "--expand", action="store_true", default=False, + help="Expand groups in comps format") + parser.add_option("--spec", action="store_true", help="Print build spec") + parser.add_option("--show-blocked", action="store_true", dest="incl_blocked", + help="Show blocked packages") + (options, args) = parser.parse_args(args) + if len(args) != 1: + parser.error("Incorrect number of arguments") + if options.incl_blocked and (options.comps or options.spec): + parser.error("--show-blocked doesn't make sense for comps/spec output") + ensure_connection(session, goptions) + tag = args[0] + callopts = {} + if options.incl_blocked: + callopts['incl_blocked'] = True + groups = session.getTagGroups(tag, **callopts) + if options.comps: + print(koji.generate_comps(groups, expand_groups=options.expand)) + elif options.spec: + print(koji.make_groups_spec(groups, name='buildgroups', buildgroup='build')) + else: + pprint.pprint(groups) diff --git a/cli/koji_cli/commands/spin_appliance.py b/cli/koji_cli/commands/spin_appliance.py new file mode 100644 index 0000000..bdd31e4 --- /dev/null +++ b/cli/koji_cli/commands/spin_appliance.py @@ -0,0 +1,55 @@ +from __future__ import absolute_import, division + +from optparse import OptionParser + + +from koji_cli.lib import ( + get_usage_str, + build_image, +) + + +def handle_spin_appliance(options, session, args): + """[build] Create an appliance given a kickstart file""" + + # Usage & option parsing + usage = "usage: %prog spin-appliance [options] " \ + "" + parser = OptionParser(usage=get_usage_str(usage)) + parser.add_option("--wait", action="store_true", + help="Wait on the appliance creation, even if running in the background") + parser.add_option("--nowait", action="store_false", dest="wait", + help="Don't wait on appliance creation") + parser.add_option("--noprogress", action="store_true", + help="Do not display progress of the upload") + parser.add_option("--background", action="store_true", + help="Run the appliance creation task at a lower priority") + parser.add_option("--ksurl", metavar="SCMURL", + help="The URL to the SCM containing the kickstart file") + parser.add_option("--ksversion", metavar="VERSION", + help="The syntax version used in the kickstart file") + parser.add_option("--scratch", action="store_true", help="Create a scratch appliance") + parser.add_option("--repo", action="append", + help="Specify a repo that will override the repo used to install " + "RPMs in the appliance. May be used multiple times. The " + "build tag repo associated with the target is the default.") + parser.add_option("--release", help="Forcibly set the release field") + parser.add_option("--specfile", metavar="URL", + help="SCM URL to spec file fragment to use to generate wrapper RPMs") + parser.add_option("--skip-tag", action="store_true", help="Do not attempt to tag package") + parser.add_option("--vmem", metavar="VMEM", default=None, + help="Set the amount of virtual memory in the appliance in MB, " + "default is 512") + parser.add_option("--vcpu", metavar="VCPU", default=None, + help="Set the number of virtual cpus in the appliance, default is 1") + parser.add_option("--format", metavar="DISK_FORMAT", default='raw', + help="Disk format, default is raw. Other options are qcow, qcow2, and vmx.") + + (task_options, args) = parser.parse_args(args) + + # Make sure the target and kickstart is specified. + print('spin-appliance is deprecated and will be replaced with image-build') + if len(args) != 5: + parser.error("Five arguments are required: a name, a version, an architecture, " + "a build target, and a relative path to a kickstart file.") + return build_image(options, task_options, session, args, 'appliance') diff --git a/cli/koji_cli/commands/spin_livecd.py b/cli/koji_cli/commands/spin_livecd.py new file mode 100644 index 0000000..4c54bc9 --- /dev/null +++ b/cli/koji_cli/commands/spin_livecd.py @@ -0,0 +1,51 @@ +from __future__ import absolute_import, division + +from optparse import OptionParser + + +from koji_cli.lib import ( + get_usage_str, + build_image, +) + + +def handle_spin_livecd(options, session, args): + """[build] Create a live CD image given a kickstart file""" + + # Usage & option parsing. + usage = "usage: %prog spin-livecd [options] " + parser = OptionParser(usage=get_usage_str(usage)) + parser.add_option("--wait", action="store_true", + help="Wait on the livecd creation, even if running in the background") + parser.add_option("--nowait", action="store_false", dest="wait", + help="Don't wait on livecd creation") + parser.add_option("--noprogress", action="store_true", + help="Do not display progress of the upload") + parser.add_option("--background", action="store_true", + help="Run the livecd creation task at a lower priority") + parser.add_option("--ksurl", metavar="SCMURL", + help="The URL to the SCM containing the kickstart file") + parser.add_option("--ksversion", metavar="VERSION", + help="The syntax version used in the kickstart file") + parser.add_option("--scratch", action="store_true", + help="Create a scratch LiveCD image") + parser.add_option("--repo", action="append", + help="Specify a repo that will override the repo used to install " + "RPMs in the LiveCD. May be used multiple times. The " + "build tag repo associated with the target is the default.") + parser.add_option("--release", help="Forcibly set the release field") + parser.add_option("--volid", help="Set the volume id") + parser.add_option("--specfile", metavar="URL", + help="SCM URL to spec file fragment to use to generate wrapper RPMs") + parser.add_option("--skip-tag", action="store_true", + help="Do not attempt to tag package") + (task_options, args) = parser.parse_args(args) + + # Make sure the target and kickstart is specified. + print('spin-livecd is deprecated and will be replaced with spin-livemedia') + if len(args) != 5: + parser.error("Five arguments are required: a name, a version, an architecture, " + "a build target, and a relative path to a kickstart file.") + if task_options.volid is not None and len(task_options.volid) > 32: + parser.error('Volume ID has a maximum length of 32 characters') + return build_image(options, task_options, session, args, 'livecd') diff --git a/cli/koji_cli/commands/spin_livemedia.py b/cli/koji_cli/commands/spin_livemedia.py new file mode 100644 index 0000000..9bf3e80 --- /dev/null +++ b/cli/koji_cli/commands/spin_livemedia.py @@ -0,0 +1,71 @@ +from __future__ import absolute_import, division + +from optparse import OptionParser + + +from koji_cli.lib import ( + get_usage_str, + build_image, +) + + +def handle_spin_livemedia(options, session, args): + """[build] Create a livemedia image given a kickstart file""" + + # Usage & option parsing. + usage = "usage: %prog spin-livemedia [options] " \ + "" + parser = OptionParser(usage=get_usage_str(usage)) + parser.add_option("--wait", action="store_true", + help="Wait on the livemedia creation, even if running in the background") + parser.add_option("--nowait", action="store_false", dest="wait", + help="Don't wait on livemedia creation") + parser.add_option("--noprogress", action="store_true", + help="Do not display progress of the upload") + parser.add_option("--background", action="store_true", + help="Run the livemedia creation task at a lower priority") + parser.add_option("--ksurl", metavar="SCMURL", + help="The URL to the SCM containing the kickstart file") + parser.add_option("--install-tree-url", metavar="URL", + help="Provide the URL for the install tree") + parser.add_option("--ksversion", metavar="VERSION", + help="The syntax version used in the kickstart file") + parser.add_option("--scratch", action="store_true", + help="Create a scratch LiveMedia image") + parser.add_option("--repo", action="append", + help="Specify a repo that will override the repo used to install " + "RPMs in the LiveMedia. May be used multiple times. The " + "build tag repo associated with the target is the default.") + parser.add_option("--release", help="Forcibly set the release field") + parser.add_option("--volid", help="Set the volume id") + parser.add_option("--specfile", metavar="URL", + help="SCM URL to spec file fragment to use to generate wrapper RPMs") + parser.add_option("--skip-tag", action="store_true", help="Do not attempt to tag package") + parser.add_option("--can-fail", action="store", dest="optional_arches", + metavar="ARCH1,ARCH2,...", default="", + help="List of archs which are not blocking for build (separated by commas.") + parser.add_option('--lorax_dir', metavar='DIR', + help='The relative path to the lorax templates ' + 'directory within the checkout of "lorax_url".') + parser.add_option('--lorax_url', metavar='URL', + help='The URL to the SCM containing any custom lorax templates that are ' + 'to be used to override the default templates.') + parser.add_option('--nomacboot', action="store_true", + help="Pass the nomacboot option to livemedia-creator") + parser.add_option('--ksrepo', action="store_true", + help="Do not overwrite repos in the kickstart") + parser.add_option('--squashfs-only', action="store_true", + help="Use a plain squashfs filesystem.") + parser.add_option('--compress-arg', action="append", default=[], metavar="ARG OPT", + help="List of compressions.") + (task_options, args) = parser.parse_args(args) + + # Make sure the target and kickstart is specified. + if len(args) != 5: + parser.error("Five arguments are required: a name, a version, a build target, " + "an architecture, and a relative path to a kickstart file.") + if task_options.lorax_url is not None and task_options.lorax_dir is None: + parser.error('The "--lorax_url" option requires that "--lorax_dir" also be used.') + if task_options.volid is not None and len(task_options.volid) > 32: + parser.error('Volume ID has a maximum length of 32 characters') + return build_image(options, task_options, session, args, 'livemedia') diff --git a/cli/koji_cli/commands/tag_build.py b/cli/koji_cli/commands/tag_build.py new file mode 100644 index 0000000..52cdf30 --- /dev/null +++ b/cli/koji_cli/commands/tag_build.py @@ -0,0 +1,37 @@ +from __future__ import absolute_import, division + +from optparse import OptionParser + + +from koji_cli.lib import ( + _running_in_bg, + activate_session, + get_usage_str, + watch_tasks +) + + +def handle_tag_build(opts, session, args): + "[bind] Apply a tag to one or more builds" + usage = "usage: %prog tag-build [options] [ ...]" + parser = OptionParser(usage=get_usage_str(usage)) + parser.add_option("--force", action="store_true", help="force operation") + parser.add_option("--wait", action="store_true", + help="Wait on task, even if running in the background") + parser.add_option("--nowait", action="store_false", dest="wait", help="Do not wait on task") + (options, args) = parser.parse_args(args) + if len(args) < 2: + parser.error( + "This command takes at least two arguments: a tag name/ID and one or more package " + "n-v-r's") + activate_session(session, opts) + tasks = [] + for pkg in args[1:]: + task_id = session.tagBuild(args[0], pkg, force=options.force) + # XXX - wait on task + tasks.append(task_id) + print("Created task %d" % task_id) + if options.wait or (options.wait is None and not _running_in_bg()): + session.logout() + return watch_tasks(session, tasks, quiet=opts.quiet, + poll_interval=opts.poll_interval, topurl=opts.topurl) diff --git a/cli/koji_cli/commands/taginfo.py b/cli/koji_cli/commands/taginfo.py new file mode 100644 index 0000000..575fac6 --- /dev/null +++ b/cli/koji_cli/commands/taginfo.py @@ -0,0 +1,116 @@ +from __future__ import absolute_import, division + +import pprint +import time +from optparse import OptionParser + +import koji + +from koji_cli.lib import ( + ensure_connection, + format_inheritance_flags, + get_usage_str +) + + +def anon_handle_taginfo(goptions, session, args): + "[info] Print basic information about a tag" + usage = "usage: %prog taginfo [options] [ ...]" + parser = OptionParser(usage=get_usage_str(usage)) + parser.add_option("--event", type='int', metavar="EVENT#", help="query at event") + parser.add_option("--ts", type='int', metavar="TIMESTAMP", + help="query at last event before timestamp") + parser.add_option("--repo", type='int', metavar="REPO#", help="query at event for a repo") + (options, args) = parser.parse_args(args) + if len(args) < 1: + parser.error("Please specify a tag") + ensure_connection(session, goptions) + event = koji.util.eventFromOpts(session, options) + event_opts = {} + if event: + event['timestr'] = time.asctime(time.localtime(event['ts'])) + print("Querying at event %(id)i (%(timestr)s)" % event) + event_opts['event'] = event['id'] + perms = dict([(p['id'], p['name']) for p in session.getAllPerms()]) + + tags = [] + for tag in args: + info = session.getBuildConfig(tag, **event_opts) + if info is None: + try: + info = session.getBuildConfig(int(tag), **event_opts) + except ValueError: + info = None + if info is None: + parser.error('No such tag: %s' % tag) + tags.append(info) + + for n, info in enumerate(tags): + if n > 0: + print('') + print("Tag: %(name)s [%(id)d]" % info) + print("Arches: %(arches)s" % info) + group_list = sorted([x['name'] for x in session.getTagGroups(info['id'], **event_opts)]) + print("Groups: " + ', '.join(group_list)) + if info.get('locked'): + print('LOCKED') + if info.get('perm_id') is not None: + perm_id = info['perm_id'] + print("Required permission: %r" % perms.get(perm_id, perm_id)) + if session.mavenEnabled(): + print("Maven support?: %s" % (info['maven_support'] and 'yes' or 'no')) + print("Include all Maven archives?: %s" % + (info['maven_include_all'] and 'yes' or 'no')) + if 'extra' in info: + print("Tag options:") + for key in sorted(info['extra'].keys()): + line = " %s : %s" % (key, pprint.pformat(info['extra'][key])) + if key in info.get('config_inheritance', {}).get('extra', []): + line = "%-30s [%s]" % (line, info['config_inheritance']['extra'][key]['name']) + print(line) + dest_targets = session.getBuildTargets(destTagID=info['id'], **event_opts) + build_targets = session.getBuildTargets(buildTagID=info['id'], **event_opts) + repos = {} + if not event: + for target in dest_targets + build_targets: + if target['build_tag'] not in repos: + repo = session.getRepo(target['build_tag']) + if repo is None: + repos[target['build_tag']] = "no active repo" + else: + repos[target['build_tag']] = "repo#%(id)i: %(creation_time)s" % repo + if dest_targets: + print("Targets that build into this tag:") + for target in dest_targets: + if event: + print(" %s (%s)" % (target['name'], target['build_tag_name'])) + else: + print(" %s (%s, %s)" % + (target['name'], target['build_tag_name'], repos[target['build_tag']])) + if build_targets: + print("This tag is a buildroot for one or more targets") + if not event: + print("Current repo: %s" % repos[info['id']]) + print("Targets that build from this tag:") + for target in build_targets: + print(" %s" % target['name']) + external_repos = session.getTagExternalRepos(tag_info=info['id'], **event_opts) + if external_repos: + print("External repos:") + for rinfo in external_repos: + if 'arches' not in rinfo: + # older hubs will not return this field + rinfo['arches'] = '-' + elif not rinfo['arches']: + rinfo['arches'] = 'inherited from tag' + # TODO else intersection of arches? + print(" %(priority)3i %(external_repo_name)s " + "(%(url)s, merge mode: %(merge_mode)s), arches: %(arches)s" % rinfo) + print("Inheritance:") + for parent in session.getInheritanceData(info['id'], **event_opts): + parent['flags'] = format_inheritance_flags(parent) + print(" %(priority)-4d %(flags)s %(name)s [%(parent_id)s]" % parent) + if parent['maxdepth'] is not None: + print(" maxdepth: %(maxdepth)s" % parent) + if parent['pkg_filter']: + print(" package filter: %(pkg_filter)s" % parent) diff --git a/cli/koji_cli/commands/taskinfo.py b/cli/koji_cli/commands/taskinfo.py new file mode 100644 index 0000000..2bbc80b --- /dev/null +++ b/cli/koji_cli/commands/taskinfo.py @@ -0,0 +1,28 @@ +from __future__ import absolute_import, division + +from optparse import OptionParser + + +from koji_cli.lib import ( + ensure_connection, + get_usage_str, + printTaskInfo, +) + + +def anon_handle_taskinfo(goptions, session, args): + """[info] Show information about a task""" + usage = "usage: %prog taskinfo [options] [ ...]" + parser = OptionParser(usage=get_usage_str(usage)) + parser.add_option("-r", "--recurse", action="store_true", + help="Show children of this task as well") + parser.add_option("-v", "--verbose", action="store_true", help="Be verbose") + (options, args) = parser.parse_args(args) + if len(args) < 1: + parser.error("You must specify at least one task ID") + + ensure_connection(session, goptions) + + for arg in args: + task_id = int(arg) + printTaskInfo(session, task_id, goptions.topdir, 0, options.recurse, options.verbose) diff --git a/cli/koji_cli/commands/unblock_group_pkg.py b/cli/koji_cli/commands/unblock_group_pkg.py new file mode 100644 index 0000000..abfebfe --- /dev/null +++ b/cli/koji_cli/commands/unblock_group_pkg.py @@ -0,0 +1,23 @@ +from __future__ import absolute_import, division + +from optparse import OptionParser + + +from koji_cli.lib import ( + activate_session, + get_usage_str +) + + +def handle_unblock_group_pkg(goptions, session, args): + "[admin] Unblock a package from a group's package listing" + usage = "usage: %prog unblock-group-pkg [options] [ ...]" + parser = OptionParser(usage=get_usage_str(usage)) + (options, args) = parser.parse_args(args) + if len(args) < 3: + parser.error("You must specify a tag name, group name, and one or more package names") + tag = args[0] + group = args[1] + activate_session(session, goptions) + for pkg in args[2:]: + session.groupPackageListUnblock(tag, group, pkg) diff --git a/cli/koji_cli/commands/unblock_group_req.py b/cli/koji_cli/commands/unblock_group_req.py new file mode 100644 index 0000000..87447b9 --- /dev/null +++ b/cli/koji_cli/commands/unblock_group_req.py @@ -0,0 +1,23 @@ +from __future__ import absolute_import, division + +from optparse import OptionParser + + +from koji_cli.lib import ( + activate_session, + get_usage_str +) + + +def handle_unblock_group_req(goptions, session, args): + "[admin] Unblock a group's requirement listing" + usage = "usage: %prog unblock-group-req [options] " + parser = OptionParser(usage=get_usage_str(usage)) + (options, args) = parser.parse_args(args) + if len(args) != 3: + parser.error("You must specify a tag name and two group names") + tag = args[0] + group = args[1] + req = args[2] + activate_session(session, goptions) + session.groupReqListUnblock(tag, group, req) diff --git a/cli/koji_cli/commands/unblock_notification.py b/cli/koji_cli/commands/unblock_notification.py new file mode 100644 index 0000000..bc91ed0 --- /dev/null +++ b/cli/koji_cli/commands/unblock_notification.py @@ -0,0 +1,31 @@ +from __future__ import absolute_import, division + +from optparse import OptionParser + + +from koji_cli.lib import ( + activate_session, + get_usage_str +) + + +def handle_unblock_notification(goptions, session, args): + "[monitor] Unblock user's notification" + usage = "usage: %prog unblock-notification [options] [ ...]" + parser = OptionParser(usage=get_usage_str(usage)) + (options, args) = parser.parse_args(args) + + activate_session(session, goptions) + + if len(args) < 1: + parser.error("At least one notification block id has to be specified") + + try: + n_ids = [int(x) for x in args] + except ValueError: + parser.error("All notification block ids has to be integers") + + for n_id in n_ids: + session.deleteNotificationBlock(n_id) + if not goptions.quiet: + print("Notification block %d successfully removed." % n_id) diff --git a/cli/koji_cli/commands/unblock_pkg.py b/cli/koji_cli/commands/unblock_pkg.py new file mode 100644 index 0000000..f3128eb --- /dev/null +++ b/cli/koji_cli/commands/unblock_pkg.py @@ -0,0 +1,23 @@ +from __future__ import absolute_import, division + +from optparse import OptionParser + + +from koji_cli.lib import ( + activate_session, + get_usage_str +) + + +def handle_unblock_pkg(goptions, session, args): + "[admin] Unblock a package in the listing for tag" + usage = "usage: %prog unblock-pkg [options] [ ...]" + parser = OptionParser(usage=get_usage_str(usage)) + (options, args) = parser.parse_args(args) + if len(args) < 2: + parser.error("Please specify a tag and at least one package") + activate_session(session, goptions) + tag = args[0] + with session.multicall(strict=True) as m: + for package in args[1:]: + m.packageListUnblock(tag, package) diff --git a/cli/koji_cli/commands/unlock_tag.py b/cli/koji_cli/commands/unlock_tag.py new file mode 100644 index 0000000..af267ac --- /dev/null +++ b/cli/koji_cli/commands/unlock_tag.py @@ -0,0 +1,51 @@ +from __future__ import absolute_import, division + +import fnmatch +from optparse import OptionParser + + +from koji_cli.lib import ( + activate_session, + get_usage_str +) + + +def handle_unlock_tag(goptions, session, args): + "[admin] Unlock a tag" + usage = "usage: %prog unlock-tag [options] [ ...]" + parser = OptionParser(usage=get_usage_str(usage)) + parser.add_option("--glob", action="store_true", help="Treat args as glob patterns") + parser.add_option("-n", "--test", action="store_true", help="Test mode") + (options, args) = parser.parse_args(args) + if len(args) < 1: + parser.error("Please specify a tag") + activate_session(session, goptions) + if options.glob: + selected = [] + for tag in session.listTags(): + for pattern in args: + if fnmatch.fnmatch(tag['name'], pattern): + selected.append(tag) + break + if not selected: + print("No tags matched") + else: + selected = [] + for name in args: + tag = session.getTag(name) + if tag is None: + parser.error("No such tag: %s" % name) + selected.append(tag) + for tag in selected: + opts = {} + if tag['locked']: + opts['locked'] = False + if tag['perm_id']: + opts['perm'] = None + if not opts: + print("Tag %(name)s: not locked" % tag) + continue + if options.test: + print("Tag %s: skipping changes: %r" % (tag['name'], opts)) + else: + session.editTag2(tag['id'], **opts) diff --git a/cli/koji_cli/commands/untag_build.py b/cli/koji_cli/commands/untag_build.py new file mode 100644 index 0000000..f308a3a --- /dev/null +++ b/cli/koji_cli/commands/untag_build.py @@ -0,0 +1,110 @@ +from __future__ import absolute_import, division + +import itertools +from optparse import OptionParser + +import koji + +from koji_cli.lib import ( + activate_session, + error, + get_usage_str, + warn +) + + +def handle_untag_build(goptions, session, args): + "[bind] Remove a tag from one or more builds" + usage = "usage: %prog untag-build [options] [ ...]" + parser = OptionParser(usage=get_usage_str(usage)) + parser.add_option("--all", action="store_true", + help="untag all versions of the package in this tag, pkg is package name") + parser.add_option("--non-latest", action="store_true", + help="untag all versions of the package in this tag except the latest, " + "pkg is package name") + parser.add_option("-n", "--test", action="store_true", help="test mode") + parser.add_option("-v", "--verbose", action="store_true", help="print details") + parser.add_option("--force", action="store_true", help="force operation") + (options, args) = parser.parse_args(args) + if options.non_latest and options.force: + if len(args) < 1: + parser.error("Please specify a tag") + elif len(args) < 2: + parser.error( + "This command takes at least two arguments: a tag name/ID and one or more package " + "n-v-r's or package names") + activate_session(session, goptions) + tag = session.getTag(args[0]) + if not tag: + parser.error("No such tag: %s" % args[0]) + if options.all: + result = [] + with session.multicall() as m: + result.extend([m.queryHistory(tag=args[0], package=pkg, active=True) + for pkg in args[1:]]) + builds = [] + for r in result: + builds.extend(r.result['tag_listing']) + elif options.non_latest: + if options.force and len(args) == 1: + tagged = session.queryHistory(tag=args[0], active=True)['tag_listing'] + tagged = sorted(tagged, key=lambda k: (k['create_event']), reverse=True) + else: + result = [] + with session.multicall() as m: + result.extend([m.queryHistory(tag=args[0], package=pkg, active=True) + for pkg in args[1:]]) + tagged = [] + for r in result: + tagged.extend(r.result['tag_listing']) + tagged = sorted(tagged, key=lambda k: (k['create_event']), reverse=True) + # listTagged orders entries latest first + seen_pkg = {} + builds = [] + for binfo in tagged: + if binfo['name'] not in seen_pkg: + # latest for this package + nvr = '%s-%s-%s' % (binfo['name'], binfo['version'], binfo['release']) + if options.verbose: + print("Leaving latest build for package %s: %s" % (binfo['name'], nvr)) + else: + builds.append(binfo) + seen_pkg[binfo['name']] = 1 + else: + # find all pkg's builds in tag + pkgs = set([koji.parse_NVR(nvr)['name'] for nvr in args[1:]]) + result = [] + with session.multicall() as m: + result.extend([m.queryHistory(tag=args[0], pkg=pkg, active=True) for pkg in pkgs]) + tagged = [] + for r in result: + tagged.append(r.result['tag_listing']) + # flatten + tagged = list(itertools.chain(*[t for t in tagged])) + idx = dict([('%s-%s-%s' % (b['name'], b['version'], b['release']), b) for b in tagged]) + + # check exact builds + builds = [] + for nvr in args[1:]: + binfo = idx.get(nvr) + if binfo: + builds.append(binfo) + else: + # not in tag, see if it even exists + binfo = session.getBuild(nvr) + if not binfo: + warn("No such build: %s" % nvr) + else: + warn("Build %s not in tag %s" % (nvr, tag['name'])) + if not options.force: + error() + builds.reverse() + with session.multicall(strict=True) as m: + for binfo in builds: + build_nvr = '%s-%s-%s' % (binfo['name'], binfo['version'], binfo['release']) + if options.test: + print("would have untagged %s" % build_nvr) + else: + if options.verbose: + print("untagging %s" % build_nvr) + m.untagBuild(tag['name'], build_nvr, force=options.force) diff --git a/cli/koji_cli/commands/userinfo.py b/cli/koji_cli/commands/userinfo.py new file mode 100644 index 0000000..8565b66 --- /dev/null +++ b/cli/koji_cli/commands/userinfo.py @@ -0,0 +1,67 @@ +from __future__ import absolute_import, division + +from optparse import OptionParser + +from six.moves import filter, zip + +import koji + +from koji_cli.lib import ( + ensure_connection, + get_usage_str, + warn +) + + +def anon_handle_userinfo(goptions, session, args): + """[admin] Show information about a user""" + usage = "usage: %prog userinfo [options] [ ...]" + parser = OptionParser(usage=get_usage_str(usage)) + (options, args) = parser.parse_args(args) + if len(args) < 1: + parser.error("You must specify at least one username") + + ensure_connection(session, goptions) + + with session.multicall() as m: + userinfos = [m.getUser(user) for user in args] + user_infos = [] + for username, userinfo in zip(args, userinfos): + if userinfo.result is None: + warn("No such user: %s\n" % username) + continue + user_infos.append(userinfo.result) + user_infos = list(filter(None, user_infos)) + + calls = [] + with session.multicall() as m: + for user in user_infos: + results = [] + if not user: + warn("No such user: %s\n" % user) + continue + results.append(m.getUserPerms(user['id'])) + results.append(m.listPackages(userID=user['id'], with_dups=True, + queryOpts={'countOnly': True})) + results.append(m.listTasks(opts={'owner': user['id'], 'parent': None}, + queryOpts={'countOnly': True})) + results.append(m.listBuilds(userID=user['id'], queryOpts={'countOnly': True})) + calls.append(results) + + for userinfo, (perms, pkgs, tasks, builds) in zip(user_infos, calls): + print("User name: %s" % userinfo['name']) + print("User ID: %d" % userinfo['id']) + if 'krb_principals' in userinfo: + print("krb principals:") + for krb in userinfo['krb_principals']: + print(" %s" % krb) + if perms.result: + print("Permissions:") + for perm in perms.result: + print(" %s" % perm) + print("Status: %s" % koji.USER_STATUS[userinfo['status']]) + print("Usertype: %s" % koji.USERTYPES[userinfo['usertype']]) + print("Number of packages: %d" % pkgs.result) + print("Number of tasks: %d" % tasks.result) + print("Number of builds: %d" % builds.result) + print('') diff --git a/cli/koji_cli/commands/version.py b/cli/koji_cli/commands/version.py new file mode 100644 index 0000000..f28b56f --- /dev/null +++ b/cli/koji_cli/commands/version.py @@ -0,0 +1,18 @@ +from __future__ import absolute_import, division + +import koji + +from koji_cli.lib import ( + ensure_connection +) + + +def handle_version(goptions, session, args): + """Report client and hub versions""" + ensure_connection(session, goptions) + print('Client: %s' % koji.__version__) + try: + version = session.getKojiVersion() + print("Hub: %s" % version) + except koji.GenericError: + print("Hub: Can't determine (older than 1.23)") diff --git a/cli/koji_cli/commands/wait_repo.py b/cli/koji_cli/commands/wait_repo.py new file mode 100644 index 0000000..f29afd1 --- /dev/null +++ b/cli/koji_cli/commands/wait_repo.py @@ -0,0 +1,107 @@ +from __future__ import absolute_import, division + +import time +from optparse import OptionParser + +import koji + +from koji_cli.lib import ( + ensure_connection, + error, + get_usage_str, + warn +) + + +def anon_handle_wait_repo(options, session, args): + "[monitor] Wait for a repo to be regenerated" + usage = "usage: %prog wait-repo [options] " + parser = OptionParser(usage=get_usage_str(usage)) + parser.add_option("--build", metavar="NVR", dest="builds", action="append", default=[], + help="Check that the given build is in the newly-generated repo " + "(may be used multiple times)") + parser.add_option("--target", action="store_true", + help="Interpret the argument as a build target name") + parser.add_option("--timeout", type="int", default=120, + help="Amount of time to wait (in minutes) before giving up " + "(default: 120)") + parser.add_option("--quiet", action="store_true", default=options.quiet, + help="Suppress output, success or failure will be indicated by the return " + "value only") + (suboptions, args) = parser.parse_args(args) + + start = time.time() + + builds = [koji.parse_NVR(build) for build in suboptions.builds] + if len(args) < 1: + parser.error("Please specify a tag name") + elif len(args) > 1: + parser.error("Only one tag may be specified") + + tag = args[0] + + ensure_connection(session, options) + if suboptions.target: + target_info = session.getBuildTarget(tag) + if not target_info: + parser.error("No such build target: %s" % tag) + tag = target_info['build_tag_name'] + tag_id = target_info['build_tag'] + else: + tag_info = session.getTag(tag) + if not tag_info: + parser.error("No such tag: %s" % tag) + targets = session.getBuildTargets(buildTagID=tag_info['id']) + if not targets: + warn("%(name)s is not a build tag for any target" % tag_info) + targets = session.getBuildTargets(destTagID=tag_info['id']) + if targets: + maybe = {}.fromkeys([t['build_tag_name'] for t in targets]) + maybe = sorted(maybe.keys()) + warn("Suggested tags: %s" % ', '.join(maybe)) + error() + tag_id = tag_info['id'] + + for nvr in builds: + data = session.getLatestBuilds(tag_id, package=nvr["name"]) + if len(data) == 0: + warn("Package %s is not in tag %s" % (nvr["name"], tag)) + else: + present_nvr = [x["nvr"] for x in data][0] + expected_nvr = '%(name)s-%(version)s-%(release)s' % nvr + if present_nvr != expected_nvr: + warn("nvr %s is not current in tag %s\n latest build in %s is %s" % + (expected_nvr, tag, tag, present_nvr)) + + last_repo = None + repo = session.getRepo(tag_id) + + while True: + if builds and repo and repo != last_repo: + if koji.util.checkForBuilds(session, tag_id, builds, repo['create_event'], + latest=True): + if not suboptions.quiet: + print("Successfully waited %s for %s to appear in the %s repo" % + (koji.util.duration(start), koji.util.printList(suboptions.builds), tag)) + return + + if (time.time() - start) >= (suboptions.timeout * 60.0): + if not suboptions.quiet: + if builds: + error("Unsuccessfully waited %s for %s to appear in the %s repo" % + (koji.util.duration(start), koji.util.printList(suboptions.builds), tag)) + else: + error("Unsuccessfully waited %s for a new %s repo" % + (koji.util.duration(start), tag)) + error() + + time.sleep(options.poll_interval) + last_repo = repo + repo = session.getRepo(tag_id) + + if not builds: + if repo != last_repo: + if not suboptions.quiet: + print("Successfully waited %s for a new %s repo" % + (koji.util.duration(start), tag)) + return diff --git a/cli/koji_cli/commands/watch_logs.py b/cli/koji_cli/commands/watch_logs.py new file mode 100644 index 0000000..261f983 --- /dev/null +++ b/cli/koji_cli/commands/watch_logs.py @@ -0,0 +1,46 @@ +from __future__ import absolute_import, division + +from optparse import OptionParser + + +from koji_cli.lib import ( + _list_tasks, + activate_session, + ensure_connection, + get_usage_str, + watch_logs +) + + +def anon_handle_watch_logs(goptions, session, args): + "[monitor] Watch logs in realtime" + usage = "usage: %prog watch-logs [options] [ ...]" + parser = OptionParser(usage=get_usage_str(usage)) + parser.add_option("--log", help="Watch only a specific log") + parser.add_option("--mine", action="store_true", + help="Watch logs for all your tasks, task_id arguments are forbidden in " + "this case.") + parser.add_option("--follow", action="store_true", help="Follow spawned child tasks") + (options, args) = parser.parse_args(args) + + if options.mine: + activate_session(session, goptions) + if args: + parser.error("Selection options cannot be combined with a task list") + tasks = _list_tasks(options, session) + tasks = [t['id'] for t in tasks] + if not tasks: + print("You've no active tasks.") + return + else: + ensure_connection(session, goptions) + tasks = [] + for task in args: + try: + tasks.append(int(task)) + except ValueError: + parser.error("task id must be an integer") + if not tasks: + parser.error("at least one task id must be specified") + + watch_logs(session, tasks, options, goptions.poll_interval) diff --git a/cli/koji_cli/commands/watch_task.py b/cli/koji_cli/commands/watch_task.py new file mode 100644 index 0000000..d5ab474 --- /dev/null +++ b/cli/koji_cli/commands/watch_task.py @@ -0,0 +1,57 @@ +from __future__ import absolute_import, division + +from optparse import OptionParser + + +from koji_cli.lib import ( + _list_tasks, + activate_session, + ensure_connection, + get_usage_str, + watch_tasks +) + + +def anon_handle_watch_task(goptions, session, args): + "[monitor] Track progress of particular tasks" + usage = "usage: %prog watch-task [options] [ ...]" + parser = OptionParser(usage=get_usage_str(usage)) + parser.add_option("--quiet", action="store_true", default=goptions.quiet, + help="Do not print the task information") + parser.add_option("--mine", action="store_true", help="Just watch your tasks") + parser.add_option("--user", help="Only tasks for this user") + parser.add_option("--arch", help="Only tasks for this architecture") + parser.add_option("--method", help="Only tasks of this method") + parser.add_option("--channel", help="Only tasks in this channel") + parser.add_option("--host", help="Only tasks for this host") + (options, args) = parser.parse_args(args) + selection = (options.mine or + options.user or + options.arch or + options.method or + options.channel or + options.host) + if args and selection: + parser.error("Selection options cannot be combined with a task list") + + if options.mine: + activate_session(session, goptions) + else: + ensure_connection(session, goptions) + if selection: + tasks = [task['id'] for task in _list_tasks(options, session)] + if not tasks: + print("(no tasks)") + return + else: + tasks = [] + for task in args: + try: + tasks.append(int(task)) + except ValueError: + parser.error("task id must be an integer") + if not tasks: + parser.error("at least one task id must be specified") + + return watch_tasks(session, tasks, quiet=options.quiet, + poll_interval=goptions.poll_interval, topurl=goptions.topurl) diff --git a/cli/koji_cli/commands/win_build.py b/cli/koji_cli/commands/win_build.py new file mode 100644 index 0000000..54a7924 --- /dev/null +++ b/cli/koji_cli/commands/win_build.py @@ -0,0 +1,84 @@ +from __future__ import absolute_import, division + +from optparse import OptionParser + + +from koji_cli.lib import ( + _running_in_bg, + activate_session, + get_usage_str, + watch_tasks +) + + +def handle_win_build(options, session, args): + """[build] Build a Windows package from source""" + # Usage & option parsing + usage = "usage: %prog win-build [options] " + parser = OptionParser(usage=get_usage_str(usage)) + parser.add_option("--winspec", metavar="URL", + help="SCM URL to retrieve the build descriptor from. " + "If not specified, the winspec must be in the root directory " + "of the source repository.") + parser.add_option("--patches", metavar="URL", + help="SCM URL of a directory containing patches to apply " + "to the sources before building") + parser.add_option("--cpus", type="int", + help="Number of cpus to allocate to the build VM " + "(requires admin access)") + parser.add_option("--mem", type="int", + help="Amount of memory (in megabytes) to allocate to the build VM " + "(requires admin access)") + parser.add_option("--static-mac", action="store_true", + help="Retain the original MAC address when cloning the VM") + parser.add_option("--specfile", metavar="URL", + help="SCM URL of a spec file fragment to use to generate wrapper RPMs") + parser.add_option("--scratch", action="store_true", + help="Perform a scratch build") + parser.add_option("--repo-id", type="int", help="Use a specific repo") + parser.add_option("--skip-tag", action="store_true", help="Do not attempt to tag package") + parser.add_option("--background", action="store_true", + help="Run the build at a lower priority") + parser.add_option("--wait", action="store_true", + help="Wait on the build, even if running in the background") + parser.add_option("--nowait", action="store_false", dest="wait", help="Don't wait on build") + parser.add_option("--quiet", action="store_true", + help="Do not print the task information", default=options.quiet) + (build_opts, args) = parser.parse_args(args) + if len(args) != 3: + parser.error( + "Exactly three arguments (a build target, a SCM URL, and a VM name) are required") + activate_session(session, options) + target = args[0] + if target.lower() == "none" and build_opts.repo_id: + target = None + build_opts.skip_tag = True + else: + build_target = session.getBuildTarget(target) + if not build_target: + parser.error("No such build target: %s" % target) + dest_tag = session.getTag(build_target['dest_tag']) + if not dest_tag: + parser.error("No such destination tag: %s" % build_target['dest_tag_name']) + if dest_tag['locked'] and not build_opts.scratch: + parser.error("Destination tag %s is locked" % dest_tag['name']) + scmurl = args[1] + vm_name = args[2] + opts = {} + for key in ('winspec', 'patches', 'cpus', 'mem', 'static_mac', + 'specfile', 'scratch', 'repo_id', 'skip_tag'): + val = getattr(build_opts, key) + if val is not None: + opts[key] = val + priority = None + if build_opts.background: + # relative to koji.PRIO_DEFAULT + priority = 5 + task_id = session.winBuild(vm_name, scmurl, target, opts, priority=priority) + if not build_opts.quiet: + print("Created task: %d" % task_id) + print("Task info: %s/taskinfo?taskID=%s" % (options.weburl, task_id)) + if build_opts.wait or (build_opts.wait is None and not _running_in_bg()): + session.logout() + return watch_tasks(session, [task_id], quiet=build_opts.quiet, + poll_interval=options.poll_interval, topurl=options.topurl) diff --git a/cli/koji_cli/commands/wrapper_rpm.py b/cli/koji_cli/commands/wrapper_rpm.py new file mode 100644 index 0000000..f1d5d4e --- /dev/null +++ b/cli/koji_cli/commands/wrapper_rpm.py @@ -0,0 +1,84 @@ +from __future__ import absolute_import, division + +from optparse import OptionParser + +import koji +from koji.util import to_list + +from koji_cli.lib import ( + _running_in_bg, + activate_session, + get_usage_str, + watch_tasks +) + + +def handle_wrapper_rpm(options, session, args): + """[build] Build wrapper rpms for any archives associated with a build.""" + usage = "usage: %prog wrapper-rpm [options] " + parser = OptionParser(usage=get_usage_str(usage)) + parser.add_option("--create-build", action="store_true", + help="Create a new build to contain wrapper rpms") + parser.add_option("--ini", action="append", dest="inis", metavar="CONFIG", default=[], + help="Pass build parameters via a .ini file") + parser.add_option("-s", "--section", + help="Get build parameters from this section of the .ini") + parser.add_option("--skip-tag", action="store_true", + help="If creating a new build, don't tag it") + parser.add_option("--scratch", action="store_true", help="Perform a scratch build") + parser.add_option("--wait", action="store_true", + help="Wait on build, even if running in the background") + parser.add_option("--nowait", action="store_false", dest="wait", help="Don't wait on build") + parser.add_option("--background", action="store_true", + help="Run the build at a lower priority") + + (build_opts, args) = parser.parse_args(args) + if build_opts.inis: + if len(args) != 1: + parser.error("Exactly one argument (a build target) is required") + else: + if len(args) < 3: + parser.error("You must provide a build target, a build ID or NVR, " + "and a SCM URL to a specfile fragment") + activate_session(session, options) + + target = args[0] + if build_opts.inis: + try: + params = koji.util.parse_maven_param(build_opts.inis, scratch=build_opts.scratch, + section=build_opts.section) + except ValueError as e: + parser.error(e.args[0]) + opts = to_list(params.values())[0] + if opts.get('type') != 'wrapper': + parser.error("Section %s does not contain a wrapper-rpm config" % + to_list(params.keys())[0]) + url = opts['scmurl'] + package = opts['buildrequires'][0] + target_info = session.getBuildTarget(target, strict=True) + latest_builds = session.getLatestBuilds(target_info['dest_tag'], package=package) + if not latest_builds: + parser.error("No build of %s in %s" % (package, target_info['dest_tag_name'])) + build_id = latest_builds[0]['nvr'] + else: + build_id = args[1] + if build_id.isdigit(): + build_id = int(build_id) + url = args[2] + priority = None + if build_opts.background: + priority = 5 + opts = {} + if build_opts.create_build: + opts['create_build'] = True + if build_opts.skip_tag: + opts['skip_tag'] = True + if build_opts.scratch: + opts['scratch'] = True + task_id = session.wrapperRPM(build_id, url, target, priority, opts=opts) + print("Created task: %d" % task_id) + print("Task info: %s/taskinfo?taskID=%s" % (options.weburl, task_id)) + if build_opts.wait or (build_opts.wait is None and not _running_in_bg()): + session.logout() + return watch_tasks(session, [task_id], quiet=options.quiet, + poll_interval=options.poll_interval, topurl=options.topurl) diff --git a/cli/koji_cli/commands/write_signed_rpm.py b/cli/koji_cli/commands/write_signed_rpm.py new file mode 100644 index 0000000..6f526fc --- /dev/null +++ b/cli/koji_cli/commands/write_signed_rpm.py @@ -0,0 +1,67 @@ +from __future__ import absolute_import, division + +from optparse import OptionParser + +from six.moves import zip + +import koji + +from koji_cli.lib import ( + activate_session, + get_usage_str +) + + +def handle_write_signed_rpm(goptions, session, args): + "[admin] Write signed RPMs to disk" + usage = "usage: %prog write-signed-rpm [options] [ ...]" + parser = OptionParser(usage=get_usage_str(usage)) + parser.add_option("--all", action="store_true", help="Write out all RPMs signed with this key") + parser.add_option("--buildid", help="Specify a build id rather than an n-v-r") + (options, args) = parser.parse_args(args) + if len(args) < 1: + parser.error("A signature key must be specified") + if len(args) < 2 and not (options.all or options.buildid): + parser.error("At least one RPM must be specified") + key = args.pop(0).lower() + activate_session(session, goptions) + if options.all: + rpms = session.queryRPMSigs(sigkey=key) + with session.multicall() as m: + results = [m.getRPM(r['rpm_id']) for r in rpms] + rpms = [x.result for x in results] + elif options.buildid: + rpms = session.listRPMs(int(options.buildid)) + else: + nvrs = [] + rpms = [] + + with session.multicall() as m: + result = [m.getRPM(nvra, strict=False) for nvra in args] + for rpm, nvra in zip(result, args): + rpm = rpm.result + if rpm: + rpms.append(rpm) + else: + nvrs.append(nvra) + + # for historical reasons, we also accept nvrs + with session.multicall() as m: + result = [m.getBuild(nvr, strict=True) for nvr in nvrs] + builds = [] + for nvr, build in zip(nvrs, result): + try: + builds.append(build.result['id']) + except koji.GenericError: + raise koji.GenericError("No such rpm or build: %s" % nvr) + + with session.multicall() as m: + rpm_lists = [m.listRPMs(buildID=build_id) for build_id in builds] + for rpm_list in rpm_lists: + rpms.extend(rpm_list.result) + + with session.multicall(strict=True) as m: + for i, rpminfo in enumerate(rpms): + nvra = "%(name)s-%(version)s-%(release)s.%(arch)s" % rpminfo + print("[%d/%d] %s" % (i + 1, len(rpms), nvra)) + m.writeSignedRPM(rpminfo['id'], key) diff --git a/cli/koji_cli/lib.py b/cli/koji_cli/lib.py index 04ef329..dbf123e 100644 --- a/cli/koji_cli/lib.py +++ b/cli/koji_cli/lib.py @@ -858,3 +858,164 @@ class DatetimeJSONEncoder(json.JSONEncoder): if isinstance(o, xmlrpc_client.DateTime): return str(o) return json.JSONEncoder.default(self, o) + + +def printable_unicode(s): + if six.PY2: + return s.encode('utf-8') + else: # no cover: 2.x + return s + + +def printTaskInfo(session, task_id, topdir, level=0, recurse=True, verbose=True): + """Recursive function to print information about a task + and its children.""" + + BUILDDIR = '/var/lib/mock' + indent = " " * 2 * level + + info = session.getTaskInfo(task_id) + + if info is None: + raise koji.GenericError("No such task: %d" % task_id) + + if info['host_id']: + host_info = session.getHost(info['host_id']) + else: + host_info = None + buildroot_infos = session.listBuildroots(taskID=task_id) + build_info = session.listBuilds(taskID=task_id) + + files = list_task_output_all_volumes(session, task_id) + logs = [] + output = [] + for filename in files: + if filename.endswith('.log'): + logs += [os.path.join(koji.pathinfo.work(volume=volume), + koji.pathinfo.taskrelpath(task_id), + filename) for volume in files[filename]] + else: + output += [os.path.join(koji.pathinfo.work(volume=volume), + koji.pathinfo.taskrelpath(task_id), + filename) for volume in files[filename]] + + owner = session.getUser(info['owner'])['name'] + + print("%sTask: %d" % (indent, task_id)) + print("%sType: %s" % (indent, info['method'])) + if verbose: + print("%sRequest Parameters:" % indent) + for line in _parseTaskParams(session, info['method'], task_id, topdir): + print("%s %s" % (indent, line)) + print("%sOwner: %s" % (indent, owner)) + print("%sState: %s" % (indent, koji.TASK_STATES[info['state']].lower())) + print("%sCreated: %s" % (indent, time.asctime(time.localtime(info['create_ts'])))) + if info.get('start_ts'): + print("%sStarted: %s" % (indent, time.asctime(time.localtime(info['start_ts'])))) + if info.get('completion_ts'): + print("%sFinished: %s" % (indent, time.asctime(time.localtime(info['completion_ts'])))) + if host_info: + print("%sHost: %s" % (indent, host_info['name'])) + if build_info: + print("%sBuild: %s (%d)" % (indent, build_info[0]['nvr'], build_info[0]['build_id'])) + if buildroot_infos: + print("%sBuildroots:" % indent) + for root in buildroot_infos: + print("%s %s/%s-%d-%d/" % + (indent, BUILDDIR, root['tag_name'], root['id'], root['repo_id'])) + if logs: + print("%sLog Files:" % indent) + for log_path in logs: + print("%s %s" % (indent, log_path)) + if output: + print("%sOutput:" % indent) + for file_path in output: + print("%s %s" % (indent, file_path)) + + # white space + print('') + + if recurse: + level += 1 + children = session.getTaskChildren(task_id, request=True) + children.sort(key=lambda x: x['id']) + for child in children: + printTaskInfo(session, child['id'], topdir, level, verbose=verbose) + + +def build_image(options, task_opts, session, args, img_type): + """ + A private helper function that houses common CLI code for building + images with chroot-based tools. + """ + + if img_type not in ('livecd', 'appliance', 'livemedia'): + raise koji.GenericError('Unrecognized image type: %s' % img_type) + activate_session(session, options) + + # Set the task's priority. Users can only lower it with --background. + priority = None + if task_opts.background: + # relative to koji.PRIO_DEFAULT; higher means a "lower" priority. + priority = 5 + if _running_in_bg() or task_opts.noprogress: + callback = None + else: + callback = _progress_callback + + # We do some early sanity checking of the given target. + # Kojid gets these values again later on, but we check now as a convenience + # for the user. + target = args[2] + tmp_target = session.getBuildTarget(target) + if not tmp_target: + raise koji.GenericError("No such build target: %s" % target) + dest_tag = session.getTag(tmp_target['dest_tag']) + if not dest_tag: + raise koji.GenericError("No such destination tag: %s" % tmp_target['dest_tag_name']) + + # Set the architecture + if img_type == 'livemedia': + # livemedia accepts multiple arches + arch = [koji.canonArch(a) for a in args[3].split(",")] + else: + arch = koji.canonArch(args[3]) + + # Upload the KS file to the staging area. + # If it's a URL, it's kojid's job to go get it when it does the checkout. + ksfile = args[4] + + if not task_opts.ksurl: + serverdir = unique_path('cli-' + img_type) + session.uploadWrapper(ksfile, serverdir, callback=callback) + ksfile = os.path.join(serverdir, os.path.basename(ksfile)) + print('') + + hub_opts = {} + passthru_opts = [ + 'format', 'install_tree_url', 'isoname', 'ksurl', + 'ksversion', 'release', 'repo', 'scratch', 'skip_tag', + 'specfile', 'vcpu', 'vmem', 'volid', 'optional_arches', + 'lorax_dir', 'lorax_url', 'nomacboot', 'ksrepo', + 'squashfs_only', 'compress_arg', + ] + for opt in passthru_opts: + val = getattr(task_opts, opt, None) + if val is not None: + hub_opts[opt] = val + + if 'optional_arches' in hub_opts: + hub_opts['optional_arches'] = hub_opts['optional_arches'].split(',') + # finally, create the task. + task_id = session.buildImage(args[0], args[1], arch, target, ksfile, + img_type, opts=hub_opts, priority=priority) + + if not options.quiet: + print("Created task: %d" % task_id) + print("Task info: %s/taskinfo?taskID=%s" % (options.weburl, task_id)) + if task_opts.wait or (task_opts.wait is None and not _running_in_bg()): + session.logout() + return watch_tasks(session, [task_id], quiet=options.quiet, + poll_interval=options.poll_interval, topurl=options.topurl) + + diff --git a/cli/koji_cli/split.py b/cli/koji_cli/split.py index 55520f7..07be871 100755 --- a/cli/koji_cli/split.py +++ b/cli/koji_cli/split.py @@ -101,9 +101,9 @@ for line in open('c.py'): fname = line[4:].split('(')[0] if fname.startswith('handle_') or fname.startswith('anon_handle_'): if fname.startswith('handle_'): - fn = f'cmd_{fname[7:]}' + fn = f'{fname[7:]}' else: - fn = f'cmd_{fname[12:]}' + fn = f'{fname[12:]}' out_cmd = open(os.path.join('commands', "%s.py" % fn), 'wt') out_cmd.write(HEAD) out_cmd.write(line) @@ -117,9 +117,9 @@ for line in open('c.py'): fname = line[4:].split('(')[0] if fname.startswith('handle_') or fname.startswith('anon_handle_'): if fname.startswith('handle_'): - fn = f'cmd_{fname[7:]}' + fn = f'{fname[7:]}' else: - fn = f'cmd_{fname[12:]}' + fn = f'{fname[12:]}' out_cmd = open(os.path.join('commands', "%s.py" % fn), 'wt') out_cmd.write(HEAD) out_cmd.write(line) From 05c6368d9317c5b2f5e4420b283a88ba7d071b44 Mon Sep 17 00:00:00 2001 From: Tester Date: Jun 13 2022 14:57:25 +0000 Subject: [PATCH 5/6] test fixes --- diff --git a/cli/koji_cli/commands/cmd_import.py b/cli/koji_cli/commands/cmd_import.py new file mode 100644 index 0000000..5af2ad4 --- /dev/null +++ b/cli/koji_cli/commands/cmd_import.py @@ -0,0 +1,163 @@ +from __future__ import absolute_import, division + +import os +import sys +from optparse import OptionParser + +import koji +from koji.util import to_list + +from koji_cli.lib import ( + activate_session, + get_usage_str, + linked_upload, + unique_path, + warn +) + + +def handle_import(goptions, session, args): + "[admin] Import externally built RPMs into the database" + usage = "usage: %prog import [options] [ ...]" + parser = OptionParser(usage=get_usage_str(usage)) + parser.add_option("--link", action="store_true", + help="Attempt to hardlink instead of uploading") + parser.add_option("--test", action="store_true", help="Don't actually import") + parser.add_option("--create-build", action="store_true", help="Auto-create builds as needed") + parser.add_option("--src-epoch", help="When auto-creating builds, use this epoch") + (options, args) = parser.parse_args(args) + if len(args) < 1: + parser.error("At least one package must be specified") + if options.src_epoch in ('None', 'none', '(none)'): + options.src_epoch = None + elif options.src_epoch: + try: + options.src_epoch = int(options.src_epoch) + except (ValueError, TypeError): + parser.error("Invalid value for epoch: %s" % options.src_epoch) + activate_session(session, goptions) + to_import = {} + for path in args: + data = koji.get_header_fields(path, ('name', 'version', 'release', 'epoch', + 'arch', 'sigmd5', 'sourcepackage', 'sourcerpm')) + if data['sourcepackage']: + data['arch'] = 'src' + nvr = "%(name)s-%(version)s-%(release)s" % data + else: + nvr = "%(name)s-%(version)s-%(release)s" % koji.parse_NVRA(data['sourcerpm']) + to_import.setdefault(nvr, []).append((path, data)) + builds_missing = False + nvrs = sorted(to_list(to_import.keys())) + for nvr in nvrs: + to_import[nvr].sort() + for path, data in to_import[nvr]: + if data['sourcepackage']: + break + else: + # no srpm included, check for build + binfo = session.getBuild(nvr) + if not binfo: + print("Missing build or srpm: %s" % nvr) + builds_missing = True + if builds_missing and not options.create_build: + print("Aborting import") + return + + # local function to help us out below + def do_import(path, data): + rinfo = dict([(k, data[k]) for k in ('name', 'version', 'release', 'arch')]) + prev = session.getRPM(rinfo) + if prev and not prev.get('external_repo_id', 0): + if prev['payloadhash'] == koji.hex_string(data['sigmd5']): + print("RPM already imported: %s" % path) + else: + warn("md5sum mismatch for %s" % path) + warn(" A different rpm with the same name has already been imported") + warn(" Existing sigmd5 is %r, your import has %r" % ( + prev['payloadhash'], koji.hex_string(data['sigmd5']))) + print("Skipping import") + return + if options.test: + print("Test mode -- skipping import for %s" % path) + return + serverdir = unique_path('cli-import') + if options.link: + linked_upload(path, serverdir) + else: + sys.stdout.write("uploading %s... " % path) + sys.stdout.flush() + session.uploadWrapper(path, serverdir) + print("done") + sys.stdout.flush() + sys.stdout.write("importing %s... " % path) + sys.stdout.flush() + try: + session.importRPM(serverdir, os.path.basename(path)) + except koji.GenericError as e: + print("\nError importing: %s" % str(e).splitlines()[-1]) + sys.stdout.flush() + else: + print("done") + sys.stdout.flush() + + for nvr in nvrs: + # check for existing build + need_build = True + binfo = session.getBuild(nvr) + if binfo: + b_state = koji.BUILD_STATES[binfo['state']] + if b_state == 'COMPLETE': + need_build = False + elif b_state in ['FAILED', 'CANCELED']: + if not options.create_build: + print("Build %s state is %s. Skipping import" % (nvr, b_state)) + continue + else: + print("Build %s exists with state=%s. Skipping import" % (nvr, b_state)) + continue + + # import srpms first, if any + for path, data in to_import[nvr]: + if data['sourcepackage']: + if binfo and b_state != 'COMPLETE': + # need to fix the state + print("Creating empty build: %s" % nvr) + b_data = koji.util.dslice(binfo, ['name', 'version', 'release']) + b_data['epoch'] = data['epoch'] + session.createEmptyBuild(**b_data) + binfo = session.getBuild(nvr) + do_import(path, data) + need_build = False + + if need_build: + # if we're doing this here, we weren't given the matching srpm + if not options.create_build: # pragma: no cover + if binfo: + # should have caught this earlier, but just in case... + b_state = koji.BUILD_STATES[binfo['state']] + print("Build %s state is %s. Skipping import" % (nvr, b_state)) + continue + else: + print("No such build: %s (include matching srpm or use " + "--create-build option to add it)" % nvr) + continue + else: + # let's make a new build + b_data = koji.parse_NVR(nvr) + if options.src_epoch: + b_data['epoch'] = options.src_epoch + else: + # pull epoch from first rpm + data = to_import[nvr][0][1] + b_data['epoch'] = data['epoch'] + if options.test: + print("Test mode -- would have created empty build: %s" % nvr) + else: + print("Creating empty build: %s" % nvr) + session.createEmptyBuild(**b_data) + binfo = session.getBuild(nvr) + + for path, data in to_import[nvr]: + if data['sourcepackage']: + continue + do_import(path, data) diff --git a/cli/koji_cli/commands/import.py b/cli/koji_cli/commands/import.py deleted file mode 100644 index 5af2ad4..0000000 --- a/cli/koji_cli/commands/import.py +++ /dev/null @@ -1,163 +0,0 @@ -from __future__ import absolute_import, division - -import os -import sys -from optparse import OptionParser - -import koji -from koji.util import to_list - -from koji_cli.lib import ( - activate_session, - get_usage_str, - linked_upload, - unique_path, - warn -) - - -def handle_import(goptions, session, args): - "[admin] Import externally built RPMs into the database" - usage = "usage: %prog import [options] [ ...]" - parser = OptionParser(usage=get_usage_str(usage)) - parser.add_option("--link", action="store_true", - help="Attempt to hardlink instead of uploading") - parser.add_option("--test", action="store_true", help="Don't actually import") - parser.add_option("--create-build", action="store_true", help="Auto-create builds as needed") - parser.add_option("--src-epoch", help="When auto-creating builds, use this epoch") - (options, args) = parser.parse_args(args) - if len(args) < 1: - parser.error("At least one package must be specified") - if options.src_epoch in ('None', 'none', '(none)'): - options.src_epoch = None - elif options.src_epoch: - try: - options.src_epoch = int(options.src_epoch) - except (ValueError, TypeError): - parser.error("Invalid value for epoch: %s" % options.src_epoch) - activate_session(session, goptions) - to_import = {} - for path in args: - data = koji.get_header_fields(path, ('name', 'version', 'release', 'epoch', - 'arch', 'sigmd5', 'sourcepackage', 'sourcerpm')) - if data['sourcepackage']: - data['arch'] = 'src' - nvr = "%(name)s-%(version)s-%(release)s" % data - else: - nvr = "%(name)s-%(version)s-%(release)s" % koji.parse_NVRA(data['sourcerpm']) - to_import.setdefault(nvr, []).append((path, data)) - builds_missing = False - nvrs = sorted(to_list(to_import.keys())) - for nvr in nvrs: - to_import[nvr].sort() - for path, data in to_import[nvr]: - if data['sourcepackage']: - break - else: - # no srpm included, check for build - binfo = session.getBuild(nvr) - if not binfo: - print("Missing build or srpm: %s" % nvr) - builds_missing = True - if builds_missing and not options.create_build: - print("Aborting import") - return - - # local function to help us out below - def do_import(path, data): - rinfo = dict([(k, data[k]) for k in ('name', 'version', 'release', 'arch')]) - prev = session.getRPM(rinfo) - if prev and not prev.get('external_repo_id', 0): - if prev['payloadhash'] == koji.hex_string(data['sigmd5']): - print("RPM already imported: %s" % path) - else: - warn("md5sum mismatch for %s" % path) - warn(" A different rpm with the same name has already been imported") - warn(" Existing sigmd5 is %r, your import has %r" % ( - prev['payloadhash'], koji.hex_string(data['sigmd5']))) - print("Skipping import") - return - if options.test: - print("Test mode -- skipping import for %s" % path) - return - serverdir = unique_path('cli-import') - if options.link: - linked_upload(path, serverdir) - else: - sys.stdout.write("uploading %s... " % path) - sys.stdout.flush() - session.uploadWrapper(path, serverdir) - print("done") - sys.stdout.flush() - sys.stdout.write("importing %s... " % path) - sys.stdout.flush() - try: - session.importRPM(serverdir, os.path.basename(path)) - except koji.GenericError as e: - print("\nError importing: %s" % str(e).splitlines()[-1]) - sys.stdout.flush() - else: - print("done") - sys.stdout.flush() - - for nvr in nvrs: - # check for existing build - need_build = True - binfo = session.getBuild(nvr) - if binfo: - b_state = koji.BUILD_STATES[binfo['state']] - if b_state == 'COMPLETE': - need_build = False - elif b_state in ['FAILED', 'CANCELED']: - if not options.create_build: - print("Build %s state is %s. Skipping import" % (nvr, b_state)) - continue - else: - print("Build %s exists with state=%s. Skipping import" % (nvr, b_state)) - continue - - # import srpms first, if any - for path, data in to_import[nvr]: - if data['sourcepackage']: - if binfo and b_state != 'COMPLETE': - # need to fix the state - print("Creating empty build: %s" % nvr) - b_data = koji.util.dslice(binfo, ['name', 'version', 'release']) - b_data['epoch'] = data['epoch'] - session.createEmptyBuild(**b_data) - binfo = session.getBuild(nvr) - do_import(path, data) - need_build = False - - if need_build: - # if we're doing this here, we weren't given the matching srpm - if not options.create_build: # pragma: no cover - if binfo: - # should have caught this earlier, but just in case... - b_state = koji.BUILD_STATES[binfo['state']] - print("Build %s state is %s. Skipping import" % (nvr, b_state)) - continue - else: - print("No such build: %s (include matching srpm or use " - "--create-build option to add it)" % nvr) - continue - else: - # let's make a new build - b_data = koji.parse_NVR(nvr) - if options.src_epoch: - b_data['epoch'] = options.src_epoch - else: - # pull epoch from first rpm - data = to_import[nvr][0][1] - b_data['epoch'] = data['epoch'] - if options.test: - print("Test mode -- would have created empty build: %s" % nvr) - else: - print("Creating empty build: %s" % nvr) - session.createEmptyBuild(**b_data) - binfo = session.getBuild(nvr) - - for path, data in to_import[nvr]: - if data['sourcepackage']: - continue - do_import(path, data) diff --git a/cli/koji_cli/commands/list_tag_inheritance.py b/cli/koji_cli/commands/list_tag_inheritance.py index 7391c8a..c5eddaa 100644 --- a/cli/koji_cli/commands/list_tag_inheritance.py +++ b/cli/koji_cli/commands/list_tag_inheritance.py @@ -11,7 +11,7 @@ from koji_cli.lib import ( ensure_connection, format_inheritance_flags, get_usage_str, - printable_unicode, + _printable_unicode, ) @@ -33,15 +33,15 @@ def _printInheritance(tags, sibdepths=None, reverse=False): if depth < currtag['currdepth']: outspacing = depth - outdepth sys.stdout.write(' ' * (outspacing * 3 - 1)) - sys.stdout.write(printable_unicode(u'\u2502')) + sys.stdout.write(_printable_unicode(u'\u2502')) outdepth = depth sys.stdout.write(' ' * ((currtag['currdepth'] - outdepth) * 3 - 1)) if siblings: - sys.stdout.write(printable_unicode(u'\u251c')) + sys.stdout.write(_printable_unicode(u'\u251c')) else: - sys.stdout.write(printable_unicode(u'\u2514')) - sys.stdout.write(printable_unicode(u'\u2500')) + sys.stdout.write(_printable_unicode(u'\u2514')) + sys.stdout.write(_printable_unicode(u'\u2500')) if reverse: sys.stdout.write('%(name)s (%(tag_id)i)\n' % currtag) else: diff --git a/cli/koji_cli/commands/moshimoshi.py b/cli/koji_cli/commands/moshimoshi.py index 1cdbaa6..4b94e37 100644 --- a/cli/koji_cli/commands/moshimoshi.py +++ b/cli/koji_cli/commands/moshimoshi.py @@ -9,7 +9,7 @@ from koji_cli.lib import ( activate_session, get_usage_str, greetings, - printable_unicode, + _printable_unicode, ) @@ -25,7 +25,7 @@ def handle_moshimoshi(options, session, args): if not u: print("Not authenticated") u = {'name': 'anonymous user'} - print("%s, %s!" % (printable_unicode(random.choice(greetings)), u["name"])) + print("%s, %s!" % (_printable_unicode(random.choice(greetings)), u["name"])) print("") print("You are using the hub at %s" % session.baseurl) authtype = u.get('authtype', getattr(session, 'authtype', None)) diff --git a/cli/koji_cli/commands/resubmit.py b/cli/koji_cli/commands/resubmit.py index e2fe014..41f8546 100644 --- a/cli/koji_cli/commands/resubmit.py +++ b/cli/koji_cli/commands/resubmit.py @@ -7,7 +7,7 @@ from koji_cli.lib import ( _running_in_bg, activate_session, get_usage_str, - printTaskInfo, + _printTaskInfo, watch_tasks, ) @@ -30,7 +30,7 @@ def handle_resubmit(goptions, session, args): taskID = int(args[0]) if not options.quiet: print("Resubmitting the following task:") - printTaskInfo(session, taskID, goptions.topdir, 0, False, True) + _printTaskInfo(session, taskID, goptions.topdir, 0, False, True) newID = session.resubmitTask(taskID) if not options.quiet: print("Resubmitted task %s as new task %s" % (taskID, newID)) diff --git a/cli/koji_cli/commands/spin_appliance.py b/cli/koji_cli/commands/spin_appliance.py index bdd31e4..5c07be3 100644 --- a/cli/koji_cli/commands/spin_appliance.py +++ b/cli/koji_cli/commands/spin_appliance.py @@ -5,7 +5,7 @@ from optparse import OptionParser from koji_cli.lib import ( get_usage_str, - build_image, + _build_image, ) @@ -52,4 +52,4 @@ def handle_spin_appliance(options, session, args): if len(args) != 5: parser.error("Five arguments are required: a name, a version, an architecture, " "a build target, and a relative path to a kickstart file.") - return build_image(options, task_options, session, args, 'appliance') + return _build_image(options, task_options, session, args, 'appliance') diff --git a/cli/koji_cli/commands/spin_livecd.py b/cli/koji_cli/commands/spin_livecd.py index 4c54bc9..9ea54f9 100644 --- a/cli/koji_cli/commands/spin_livecd.py +++ b/cli/koji_cli/commands/spin_livecd.py @@ -2,10 +2,9 @@ from __future__ import absolute_import, division from optparse import OptionParser - from koji_cli.lib import ( get_usage_str, - build_image, + _build_image, ) @@ -48,4 +47,4 @@ def handle_spin_livecd(options, session, args): "a build target, and a relative path to a kickstart file.") if task_options.volid is not None and len(task_options.volid) > 32: parser.error('Volume ID has a maximum length of 32 characters') - return build_image(options, task_options, session, args, 'livecd') + return _build_image(options, task_options, session, args, 'livecd') diff --git a/cli/koji_cli/commands/spin_livemedia.py b/cli/koji_cli/commands/spin_livemedia.py index 9bf3e80..08ea618 100644 --- a/cli/koji_cli/commands/spin_livemedia.py +++ b/cli/koji_cli/commands/spin_livemedia.py @@ -5,7 +5,7 @@ from optparse import OptionParser from koji_cli.lib import ( get_usage_str, - build_image, + _build_image, ) @@ -68,4 +68,4 @@ def handle_spin_livemedia(options, session, args): parser.error('The "--lorax_url" option requires that "--lorax_dir" also be used.') if task_options.volid is not None and len(task_options.volid) > 32: parser.error('Volume ID has a maximum length of 32 characters') - return build_image(options, task_options, session, args, 'livemedia') + return _build_image(options, task_options, session, args, 'livemedia') diff --git a/cli/koji_cli/commands/taskinfo.py b/cli/koji_cli/commands/taskinfo.py index 2bbc80b..b609d02 100644 --- a/cli/koji_cli/commands/taskinfo.py +++ b/cli/koji_cli/commands/taskinfo.py @@ -6,7 +6,7 @@ from optparse import OptionParser from koji_cli.lib import ( ensure_connection, get_usage_str, - printTaskInfo, + _printTaskInfo, ) @@ -25,4 +25,4 @@ def anon_handle_taskinfo(goptions, session, args): for arg in args: task_id = int(arg) - printTaskInfo(session, task_id, goptions.topdir, 0, options.recurse, options.verbose) + _printTaskInfo(session, task_id, goptions.topdir, 0, options.recurse, options.verbose) diff --git a/cli/koji_cli/lib.py b/cli/koji_cli/lib.py index dbf123e..7e17823 100644 --- a/cli/koji_cli/lib.py +++ b/cli/koji_cli/lib.py @@ -3,6 +3,7 @@ from __future__ import absolute_import, division import hashlib import json +import logging import optparse import os import random @@ -10,6 +11,7 @@ import socket import string import sys import time +import traceback from contextlib import closing from copy import copy @@ -860,14 +862,189 @@ class DatetimeJSONEncoder(json.JSONEncoder): return json.JSONEncoder.default(self, o) -def printable_unicode(s): +def _printable_unicode(s): if six.PY2: return s.encode('utf-8') else: # no cover: 2.x return s -def printTaskInfo(session, task_id, topdir, level=0, recurse=True, verbose=True): +def _handleMap(lines, data, prefix=''): + for key, val in data.items(): + if key != '__starstar': + lines.append(' %s%s: %s' % (prefix, key, val)) + + +def _handleOpts(lines, opts, prefix=''): + if opts: + lines.append("%sOptions:" % prefix) + _handleMap(lines, opts, prefix) + + +def _parseTaskParams(session, method, task_id, topdir): + try: + return _do_parseTaskParams(session, method, task_id, topdir) + except Exception: + logger = logging.getLogger("koji") + if logger.isEnabledFor(logging.DEBUG): + tb_str = ''.join(traceback.format_exception(*sys.exc_info())) + logger.debug(tb_str) + return ['Unable to parse task parameters'] + + +def _do_parseTaskParams(session, method, task_id, topdir): + """Parse the return of getTaskRequest()""" + params = session.getTaskRequest(task_id) + + lines = [] + + if method == 'buildSRPMFromCVS': + lines.append("CVS URL: %s" % params[0]) + elif method == 'buildSRPMFromSCM': + lines.append("SCM URL: %s" % params[0]) + elif method == 'buildArch': + lines.append("SRPM: %s/work/%s" % (topdir, params[0])) + lines.append("Build Tag: %s" % session.getTag(params[1])['name']) + lines.append("Build Arch: %s" % params[2]) + lines.append("SRPM Kept: %r" % params[3]) + if len(params) > 4: + _handleOpts(lines, params[4]) + elif method == 'tagBuild': + build = session.getBuild(params[1]) + lines.append("Destination Tag: %s" % session.getTag(params[0])['name']) + lines.append("Build: %s" % koji.buildLabel(build)) + elif method == 'buildNotification': + build = params[1] + buildTarget = params[2] + lines.append("Recipients: %s" % (", ".join(params[0]))) + lines.append("Build: %s" % koji.buildLabel(build)) + lines.append("Build Target: %s" % buildTarget['name']) + lines.append("Web URL: %s" % params[3]) + elif method == 'build': + lines.append("Source: %s" % params[0]) + lines.append("Build Target: %s" % params[1]) + if len(params) > 2: + _handleOpts(lines, params[2]) + elif method == 'maven': + lines.append("SCM URL: %s" % params[0]) + lines.append("Build Target: %s" % params[1]) + if len(params) > 2: + _handleOpts(lines, params[2]) + elif method == 'buildMaven': + lines.append("SCM URL: %s" % params[0]) + lines.append("Build Tag: %s" % params[1]['name']) + if len(params) > 2: + _handleOpts(lines, params[2]) + elif method == 'wrapperRPM': + lines.append("Spec File URL: %s" % params[0]) + lines.append("Build Tag: %s" % params[1]['name']) + if params[2]: + lines.append("Build: %s" % koji.buildLabel(params[2])) + if params[3]: + lines.append("Task: %s %s" % (params[3]['id'], koji.taskLabel(params[3]))) + if len(params) > 4: + _handleOpts(lines, params[4]) + elif method == 'chainmaven': + lines.append("Builds:") + for package, opts in params[0].items(): + lines.append(" " + package) + _handleMap(lines, opts, prefix=" ") + lines.append("Build Target: %s" % params[1]) + if len(params) > 2: + _handleOpts(lines, params[2]) + elif method == 'winbuild': + lines.append("VM: %s" % params[0]) + lines.append("SCM URL: %s" % params[1]) + lines.append("Build Target: %s" % params[2]) + if len(params) > 3: + _handleOpts(lines, params[3]) + elif method == 'vmExec': + lines.append("VM: %s" % params[0]) + lines.append("Exec Params:") + for info in params[1]: + if isinstance(info, dict): + _handleMap(lines, info, prefix=' ') + else: + lines.append(" %s" % info) + if len(params) > 2: + _handleOpts(lines, params[2]) + elif method in ('createLiveCD', 'createAppliance', 'createLiveMedia'): + argnames = ['Name', 'Version', 'Release', 'Arch', 'Target Info', 'Build Tag', 'Repo', + 'Kickstart File'] + for n, v in zip(argnames, params): + lines.append("%s: %s" % (n, v)) + if len(params) > 8: + _handleOpts(lines, params[8]) + elif method in ('appliance', 'livecd', 'livemedia'): + argnames = ['Name', 'Version', 'Arches', 'Target', 'Kickstart'] + for n, v in zip(argnames, params): + lines.append("%s: %s" % (n, v)) + if len(params) > 5: + _handleOpts(lines, params[5]) + elif method == 'newRepo': + tag = session.getTag(params[0]) + lines.append("Tag: %s" % tag['name']) + elif method == 'prepRepo': + lines.append("Tag: %s" % params[0]['name']) + elif method == 'createrepo': + lines.append("Repo ID: %i" % params[0]) + lines.append("Arch: %s" % params[1]) + oldrepo = params[2] + if oldrepo: + lines.append("Old Repo ID: %i" % oldrepo['id']) + lines.append("Old Repo Creation: %s" % koji.formatTimeLong(oldrepo['create_ts'])) + if len(params) > 3: + lines.append("External Repos: %s" % + ', '.join([ext['external_repo_name'] for ext in params[3]])) + elif method == 'tagNotification': + destTag = session.getTag(params[2]) + srcTag = None + if params[3]: + srcTag = session.getTag(params[3]) + build = session.getBuild(params[4]) + user = session.getUser(params[5]) + + lines.append("Recipients: %s" % ", ".join(params[0])) + lines.append("Successful?: %s" % (params[1] and 'yes' or 'no')) + lines.append("Tagged Into: %s" % destTag['name']) + if srcTag: + lines.append("Moved From: %s" % srcTag['name']) + lines.append("Build: %s" % koji.buildLabel(build)) + lines.append("Tagged By: %s" % user['name']) + lines.append("Ignore Success?: %s" % (params[6] and 'yes' or 'no')) + if params[7]: + lines.append("Failure Message: %s" % params[7]) + elif method == 'dependantTask': + lines.append("Dependant Tasks: %s" % ", ".join([str(depID) for depID in params[0]])) + lines.append("Subtasks:") + for subtask in params[1]: + lines.append(" Method: %s" % subtask[0]) + lines.append(" Parameters: %s" % + ", ".join([str(subparam) for subparam in subtask[1]])) + if len(subtask) > 2 and subtask[2]: + subopts = subtask[2] + _handleOpts(lines, subopts, prefix=' ') + lines.append("") + elif method == 'chainbuild': + lines.append("Build Groups:") + group_num = 0 + for group_list in params[0]: + group_num += 1 + lines.append(" %i: %s" % (group_num, ', '.join(group_list))) + lines.append("Build Target: %s" % params[1]) + if len(params) > 2: + _handleOpts(lines, params[2]) + elif method == 'waitrepo': + lines.append("Build Target: %s" % params[0]) + if params[1]: + lines.append("Newer Than: %s" % params[1]) + if params[2]: + lines.append("NVRs: %s" % ', '.join(params[2])) + + return lines + + +def _printTaskInfo(session, task_id, topdir, level=0, recurse=True, verbose=True): """Recursive function to print information about a task and its children.""" @@ -940,10 +1117,10 @@ def printTaskInfo(session, task_id, topdir, level=0, recurse=True, verbose=True) children = session.getTaskChildren(task_id, request=True) children.sort(key=lambda x: x['id']) for child in children: - printTaskInfo(session, child['id'], topdir, level, verbose=verbose) + _printTaskInfo(session, child['id'], topdir, level, verbose=verbose) -def build_image(options, task_opts, session, args, img_type): +def _build_image(options, task_opts, session, args, img_type): """ A private helper function that houses common CLI code for building images with chroot-based tools. @@ -1017,5 +1194,3 @@ def build_image(options, task_opts, session, args, img_type): session.logout() return watch_tasks(session, [task_id], quiet=options.quiet, poll_interval=options.poll_interval, topurl=options.topurl) - - diff --git a/cli/koji_cli/split.py b/cli/koji_cli/split.py index 07be871..4a039f0 100755 --- a/cli/koji_cli/split.py +++ b/cli/koji_cli/split.py @@ -84,7 +84,7 @@ for loader, name, is_pkg in pkgutil.walk_packages(__path__): try: shutil.rmtree('commands') -except: +except IOError: pass os.mkdir('commands') diff --git a/tests/test_cli/test_add_channel.py b/tests/test_cli/test_add_channel.py index 48c1695..4b60977 100644 --- a/tests/test_cli/test_add_channel.py +++ b/tests/test_cli/test_add_channel.py @@ -6,7 +6,7 @@ import mock import six import koji -from koji_cli.commands import handle_add_channel +from koji_cli.commands.add_channel import handle_add_channel from . import utils @@ -19,7 +19,7 @@ class TestAddChannel(utils.CliTestCase): self.channel_id = 1 self.options = mock.MagicMock() self.session = mock.MagicMock() - self.activate_session_mock = mock.patch('koji_cli.commands.activate_session').start() + self.activate_session_mock = mock.patch('koji_cli.commands.add_channel.activate_session').start() self.error_format = """Usage: %s add-channel [options] (Specify the --help global option for a list of other help options) diff --git a/tests/test_cli/test_add_group.py b/tests/test_cli/test_add_group.py index 2e6a82a..ff9db71 100644 --- a/tests/test_cli/test_add_group.py +++ b/tests/test_cli/test_add_group.py @@ -4,7 +4,7 @@ import mock import six import unittest -from koji_cli.commands import handle_add_group +from koji_cli.commands.add_group import handle_add_group from . import utils @@ -14,7 +14,7 @@ class TestAddGroup(utils.CliTestCase): maxDiff = None def setUp(self): - self.activate_session_mock = mock.patch('koji_cli.commands.activate_session').start() + self.activate_session_mock = mock.patch('koji_cli.commands.add_group.activate_session').start() self.error_format = """Usage: %s add-group (Specify the --help global option for a list of other help options) diff --git a/tests/test_cli/test_add_group_pkg.py b/tests/test_cli/test_add_group_pkg.py index dd2fa26..88a8861 100644 --- a/tests/test_cli/test_add_group_pkg.py +++ b/tests/test_cli/test_add_group_pkg.py @@ -2,7 +2,7 @@ from __future__ import absolute_import import mock import unittest -from koji_cli.commands import handle_add_group_pkg +from koji_cli.commands.add_group_pkg import handle_add_group_pkg from . import utils @@ -14,7 +14,7 @@ class TestAddGroupPkg(utils.CliTestCase): def setUp(self): self.session = mock.MagicMock() self.options = mock.MagicMock() - self.activate_session = mock.patch('koji_cli.commands.activate_session').start() + self.activate_session = mock.patch('koji_cli.commands.add_group_pkg.activate_session').start() self.error_format = """Usage: %s add-group-pkg [options] [ ...] (Specify the --help global option for a list of other help options) diff --git a/tests/test_cli/test_add_group_req.py b/tests/test_cli/test_add_group_req.py index 9502bb7..7235b39 100644 --- a/tests/test_cli/test_add_group_req.py +++ b/tests/test_cli/test_add_group_req.py @@ -2,7 +2,7 @@ from __future__ import absolute_import import mock import unittest -from koji_cli.commands import handle_add_group_req +from koji_cli.commands.add_group_req import handle_add_group_req from . import utils @@ -14,7 +14,7 @@ class TestAddGroupReq(utils.CliTestCase): def setUp(self): self.session = mock.MagicMock() self.options = mock.MagicMock() - self.activate_session = mock.patch('koji_cli.commands.activate_session').start() + self.activate_session = mock.patch('koji_cli.commands.add_group_req.activate_session').start() self.error_format = """Usage: %s add-group-req [options] (Specify the --help global option for a list of other help options) diff --git a/tests/test_cli/test_add_host.py b/tests/test_cli/test_add_host.py index 7a71469..8748cd2 100644 --- a/tests/test_cli/test_add_host.py +++ b/tests/test_cli/test_add_host.py @@ -4,7 +4,7 @@ import mock import six import koji -from koji_cli.commands import handle_add_host +from koji_cli.commands.add_host import handle_add_host from . import utils @@ -12,7 +12,7 @@ class TestAddHost(utils.CliTestCase): def setUp(self): self.maxDiff = None - self.activate_session_mock = mock.patch('koji_cli.commands.activate_session').start() + self.activate_session_mock = mock.patch('koji_cli.commands.add_host.activate_session').start() self.error_format = """Usage: %s add-host [options] [ ...] (Specify the --help global option for a list of other help options) diff --git a/tests/test_cli/test_add_host_to_channel.py b/tests/test_cli/test_add_host_to_channel.py index 1f47aa0..6422fd2 100644 --- a/tests/test_cli/test_add_host_to_channel.py +++ b/tests/test_cli/test_add_host_to_channel.py @@ -4,7 +4,7 @@ import mock import six import unittest -from koji_cli.commands import handle_add_host_to_channel +from koji_cli.commands.add_host_to_channel import handle_add_host_to_channel from . import utils @@ -12,7 +12,7 @@ class TestAddHostToChannel(utils.CliTestCase): def setUp(self): self.maxDiff = None - self.activate_session_mock = mock.patch('koji_cli.commands.activate_session').start() + self.activate_session_mock = mock.patch('koji_cli.commands.add_host_to_channel.activate_session').start() self.error_format = """Usage: %s add-host-to-channel [options] (Specify the --help global option for a list of other help options) @@ -159,7 +159,7 @@ class TestAddHostToChannel(utils.CliTestCase): @mock.patch('sys.stdout', new_callable=six.StringIO) @mock.patch('sys.stderr', new_callable=six.StringIO) - @mock.patch('koji_cli.commands.activate_session') + @mock.patch('koji_cli.commands.add_host_to_channel.activate_session') def test_handle_add_host_to_channel_help( self, activate_session_mock, stderr, stdout): arguments = [] diff --git a/tests/test_cli/test_add_notification.py b/tests/test_cli/test_add_notification.py index 212c97d..74ad560 100644 --- a/tests/test_cli/test_add_notification.py +++ b/tests/test_cli/test_add_notification.py @@ -3,7 +3,7 @@ import koji import mock from six.moves import StringIO -from koji_cli.commands import handle_add_notification +from koji_cli.commands.add_notification import handle_add_notification from . import utils @@ -14,7 +14,7 @@ class TestAddNotification(utils.CliTestCase): self.options.debug = False self.session = mock.MagicMock() self.session.getAPIVersion.return_value = koji.API_VERSION - self.activate_session_mock = mock.patch('koji_cli.commands.activate_session').start() + self.activate_session_mock = mock.patch('koji_cli.commands.add_notification.activate_session').start() self.error_format = """Usage: %s add-notification [options] (Specify the --help global option for a list of other help options) diff --git a/tests/test_cli/test_add_pkg.py b/tests/test_cli/test_add_pkg.py index a220e9f..effea4e 100644 --- a/tests/test_cli/test_add_pkg.py +++ b/tests/test_cli/test_add_pkg.py @@ -7,7 +7,7 @@ import six import koji from mock import call -from koji_cli.commands import handle_add_pkg +from koji_cli.commands.add_pkg import handle_add_pkg from . import utils @@ -20,7 +20,7 @@ class TestAddPkg(utils.CliTestCase): self.options.debug = False self.session = mock.MagicMock() self.session.getAPIVersion.return_value = koji.API_VERSION - self.activate_session_mock = mock.patch('koji_cli.commands.activate_session').start() + self.activate_session_mock = mock.patch('koji_cli.commands.add_pkg.activate_session').start() self.error_format = """Usage: %s add-pkg [options] --owner [ ...] (Specify the --help global option for a list of other help options) diff --git a/tests/test_cli/test_add_tag.py b/tests/test_cli/test_add_tag.py index c5d8640..c797570 100644 --- a/tests/test_cli/test_add_tag.py +++ b/tests/test_cli/test_add_tag.py @@ -2,7 +2,7 @@ from __future__ import absolute_import import mock import unittest -from koji_cli.commands import handle_add_tag +from koji_cli.commands.add_tag import handle_add_tag from . import utils @@ -14,7 +14,7 @@ class TestAddTag(utils.CliTestCase): self.options.quiet = True self.options.debug = False self.session = mock.MagicMock() - self.activate_session_mock = mock.patch('koji_cli.commands.activate_session').start() + self.activate_session_mock = mock.patch('koji_cli.commands.add_tag.activate_session').start() self.error_format = """Usage: %s add-tag [options] (Specify the --help global option for a list of other help options) diff --git a/tests/test_cli/test_add_tag_inheritance.py b/tests/test_cli/test_add_tag_inheritance.py index 0e146c4..01dc90e 100644 --- a/tests/test_cli/test_add_tag_inheritance.py +++ b/tests/test_cli/test_add_tag_inheritance.py @@ -3,7 +3,7 @@ from __future__ import absolute_import import mock import koji -from koji_cli.commands import handle_add_tag_inheritance +from koji_cli.commands.add_tag_inheritance import handle_add_tag_inheritance from . import utils @@ -13,7 +13,7 @@ class TestAddTagInheritance(utils.CliTestCase): self.options.debug = False self.session = mock.MagicMock() self.session.getAPIVersion.return_value = koji.API_VERSION - self.activate_session_mock = mock.patch('koji_cli.commands.activate_session').start() + self.activate_session_mock = mock.patch('koji_cli.commands.add_tag_inheritance.activate_session').start() self.error_format = """Usage: %s add-tag-inheritance [options] (Specify the --help global option for a list of other help options) diff --git a/tests/test_cli/test_add_target.py b/tests/test_cli/test_add_target.py index a453cdc..23150c0 100644 --- a/tests/test_cli/test_add_target.py +++ b/tests/test_cli/test_add_target.py @@ -3,7 +3,7 @@ from __future__ import absolute_import import mock import koji -from koji_cli.commands import handle_add_target +from koji_cli.commands.add_target import handle_add_target from . import utils @@ -13,7 +13,7 @@ class TestAddTarget(utils.CliTestCase): self.options.debug = False self.session = mock.MagicMock() self.session.getAPIVersion.return_value = koji.API_VERSION - self.activate_session_mock = mock.patch('koji_cli.commands.activate_session').start() + self.activate_session_mock = mock.patch('koji_cli.commands.add_target.activate_session').start() self.error_format = """Usage: %s add-target (Specify the --help global option for a list of other help options) diff --git a/tests/test_cli/test_add_user.py b/tests/test_cli/test_add_user.py index a7099d7..6582522 100644 --- a/tests/test_cli/test_add_user.py +++ b/tests/test_cli/test_add_user.py @@ -4,7 +4,7 @@ import six import unittest import koji -from koji_cli.commands import handle_add_user +from koji_cli.commands.add_user import handle_add_user from . import utils @@ -18,7 +18,7 @@ class TestAddUser(utils.CliTestCase): self.options.debug = False self.session = mock.MagicMock() self.session.getAPIVersion.return_value = koji.API_VERSION - self.activate_session_mock = mock.patch('koji_cli.commands.activate_session').start() + self.activate_session_mock = mock.patch('koji_cli.commands.add_user.activate_session').start() self.error_format = """Usage: %s add-user [options] (Specify the --help global option for a list of other help options) diff --git a/tests/test_cli/test_add_volume.py b/tests/test_cli/test_add_volume.py index 5fa8436..2fe6823 100644 --- a/tests/test_cli/test_add_volume.py +++ b/tests/test_cli/test_add_volume.py @@ -4,7 +4,7 @@ import six import unittest import koji -from koji_cli.commands import handle_add_volume +from koji_cli.commands.add_volume import handle_add_volume from . import utils @@ -17,7 +17,7 @@ class TestAddVolume(utils.CliTestCase): self.options.debug = False self.session = mock.MagicMock() self.session.getAPIVersion.return_value = koji.API_VERSION - self.activate_session_mock = mock.patch('koji_cli.commands.activate_session').start() + self.activate_session_mock = mock.patch('koji_cli.commands.add_volume.activate_session').start() self.error_format = """Usage: %s add-volume (Specify the --help global option for a list of other help options) diff --git a/tests/test_cli/test_assign_task.py b/tests/test_cli/test_assign_task.py index 53c69bc..7d117aa 100644 --- a/tests/test_cli/test_assign_task.py +++ b/tests/test_cli/test_assign_task.py @@ -4,7 +4,7 @@ import six import unittest import koji -from koji_cli.commands import handle_assign_task +from koji_cli.commands.assign_task import handle_assign_task from . import utils @@ -17,7 +17,7 @@ class TestAssignTask(utils.CliTestCase): self.options.debug = False self.session = mock.MagicMock() self.session.getAPIVersion.return_value = koji.API_VERSION - self.activate_session_mock = mock.patch('koji_cli.commands.activate_session').start() + self.activate_session_mock = mock.patch('koji_cli.commands.assign_task.activate_session').start() self.error_format = """Usage: %s assign-task (Specify the --help global option for a list of other help options) diff --git a/tests/test_cli/test_block_group.py b/tests/test_cli/test_block_group.py index e3583a5..e11ee76 100644 --- a/tests/test_cli/test_block_group.py +++ b/tests/test_cli/test_block_group.py @@ -4,7 +4,7 @@ import mock import six import koji -from koji_cli.commands import handle_block_group +from koji_cli.commands.block_group import handle_block_group from . import utils @@ -18,7 +18,7 @@ class TestBlockGroup(utils.CliTestCase): self.options.debug = False self.session = mock.MagicMock() self.session.getAPIVersion.return_value = koji.API_VERSION - self.activate_session_mock = mock.patch('koji_cli.commands.activate_session').start() + self.activate_session_mock = mock.patch('koji_cli.commands.block_group.activate_session').start() self.error_format = """Usage: %s block-group (Specify the --help global option for a list of other help options) diff --git a/tests/test_cli/test_block_group_pkg.py b/tests/test_cli/test_block_group_pkg.py index 52d6d60..1709b83 100644 --- a/tests/test_cli/test_block_group_pkg.py +++ b/tests/test_cli/test_block_group_pkg.py @@ -2,7 +2,7 @@ from __future__ import absolute_import import mock import unittest -from koji_cli.commands import handle_block_group_pkg +from koji_cli.commands.block_group_pkg import handle_block_group_pkg from . import utils @@ -14,7 +14,7 @@ class TestBlockGroupPkg(utils.CliTestCase): def setUp(self): self.session = mock.MagicMock() self.options = mock.MagicMock() - self.activate_session = mock.patch('koji_cli.commands.activate_session').start() + self.activate_session = mock.patch('koji_cli.commands.block_group_pkg.activate_session').start() self.error_format = """Usage: %s block-group-pkg [options] [ ...] Note that blocking is propagated through the inheritance chain, so it is not exactly the same as package removal. diff --git a/tests/test_cli/test_block_group_req.py b/tests/test_cli/test_block_group_req.py index 47a79fb..78d0243 100644 --- a/tests/test_cli/test_block_group_req.py +++ b/tests/test_cli/test_block_group_req.py @@ -2,7 +2,7 @@ from __future__ import absolute_import import mock import unittest -from koji_cli.commands import handle_block_group_req +from koji_cli.commands.block_group_req import handle_block_group_req from . import utils @@ -14,7 +14,7 @@ class TestBlockGroupReq(utils.CliTestCase): def setUp(self): self.session = mock.MagicMock() self.options = mock.MagicMock() - self.activate_session = mock.patch('koji_cli.commands.activate_session').start() + self.activate_session = mock.patch('koji_cli.commands.block_group_req.activate_session').start() self.error_format = """Usage: %s block-group-req [options] (Specify the --help global option for a list of other help options) diff --git a/tests/test_cli/test_block_notification.py b/tests/test_cli/test_block_notification.py index 6d93145..7622044 100644 --- a/tests/test_cli/test_block_notification.py +++ b/tests/test_cli/test_block_notification.py @@ -4,7 +4,7 @@ import mock from six.moves import StringIO import koji -from koji_cli.commands import handle_block_notification +from koji_cli.commands.block_notification import handle_block_notification from . import utils @@ -14,7 +14,7 @@ class TestBlockNotification(utils.CliTestCase): self.options.debug = False self.session = mock.MagicMock() self.session.getAPIVersion.return_value = koji.API_VERSION - self.activate_session_mock = mock.patch('koji_cli.commands.activate_session').start() + self.activate_session_mock = mock.patch('koji_cli.commands.block_notification.activate_session').start() self.error_format = """Usage: %s block-notification [options] (Specify the --help global option for a list of other help options) diff --git a/tests/test_cli/test_block_pkg.py b/tests/test_cli/test_block_pkg.py index 74428c9..f65564b 100644 --- a/tests/test_cli/test_block_pkg.py +++ b/tests/test_cli/test_block_pkg.py @@ -4,7 +4,7 @@ import mock import six from mock import call -from koji_cli.commands import handle_block_pkg +from koji_cli.commands.block_pkg import handle_block_pkg import koji from . import utils @@ -19,7 +19,7 @@ class TestBlockPkg(utils.CliTestCase): self.options.debug = False self.session = mock.MagicMock() self.session.getAPIVersion.return_value = koji.API_VERSION - self.activate_session_mock = mock.patch('koji_cli.commands.activate_session').start() + self.activate_session_mock = mock.patch('koji_cli.commands.block_pkg.activate_session').start() self.error_format = """Usage: %s block-pkg [options] [ ...] (Specify the --help global option for a list of other help options) diff --git a/tests/test_cli/test_build.py b/tests/test_cli/test_build.py index ef9918c..b0c4365 100644 --- a/tests/test_cli/test_build.py +++ b/tests/test_cli/test_build.py @@ -2,7 +2,8 @@ from __future__ import absolute_import import mock import six -from koji_cli.commands import handle_build, _progress_callback +from koji_cli.commands.build import handle_build +from koji_cli.lib import _progress_callback from . import utils @@ -27,12 +28,12 @@ class TestBuild(utils.CliTestCase): self.source_scm = 'http://scm' self.task_id = 1 self.priority = None - self.activate_session_mock = mock.patch('koji_cli.commands.activate_session').start() - self.unique_path_mock = mock.patch('koji_cli.commands.unique_path').start() + self.activate_session_mock = mock.patch('koji_cli.commands.build.activate_session').start() + self.unique_path_mock = mock.patch('koji_cli.commands.build.unique_path').start() self.unique_path_mock.return_value = 'random_path' - self.running_in_bg_mock = mock.patch('koji_cli.commands._running_in_bg').start() + self.running_in_bg_mock = mock.patch('koji_cli.commands.build._running_in_bg').start() self.running_in_bg_mock.return_value = False - self.watch_tasks_mock = mock.patch('koji_cli.commands.watch_tasks').start() + self.watch_tasks_mock = mock.patch('koji_cli.commands.build.watch_tasks').start() self.watch_tasks_mock.return_value = 0 self.error_format = """Usage: %s build [options] @@ -496,7 +497,7 @@ Task info: weburl/taskinfo?taskID=1 self.assertEqual(rv, 0) @mock.patch('sys.stdout', new_callable=six.StringIO) - @mock.patch('koji_cli.commands._running_in_bg', return_value=True) + @mock.patch('koji_cli.commands.build._running_in_bg', return_value=True) def test_handle_build_running_in_bg(self, running_in_bg_mock, stdout): args = [self.target, self.source_srpm] opts = {'custom_user_metadata': {}, 'wait_builds': []} diff --git a/tests/test_cli/test_buildinfo.py b/tests/test_cli/test_buildinfo.py index efcd8f6..bc69d6a 100644 --- a/tests/test_cli/test_buildinfo.py +++ b/tests/test_cli/test_buildinfo.py @@ -7,7 +7,7 @@ import copy import locale from six.moves import StringIO -from koji_cli.commands import anon_handle_buildinfo +from koji_cli.commands.buildinfo import anon_handle_buildinfo from . import utils diff --git a/tests/test_cli/test_call.py b/tests/test_cli/test_call.py index fb70d78..e3cd6cf 100644 --- a/tests/test_cli/test_call.py +++ b/tests/test_cli/test_call.py @@ -5,7 +5,7 @@ import six import unittest import koji -from koji_cli.commands import handle_call +from koji_cli.commands.call import handle_call from . import utils @@ -19,7 +19,7 @@ class TestCall(utils.CliTestCase): self.options.debug = False self.session = mock.MagicMock() self.session.getAPIVersion.return_value = koji.API_VERSION - self.activate_session_mock = mock.patch('koji_cli.commands.activate_session').start() + self.activate_session_mock = mock.patch('koji_cli.commands.call.activate_session').start() self.error_format = """Usage: %s call [options] [ ...] Note, that you can use global option --noauth for anonymous calls here @@ -101,7 +101,7 @@ Note, that you can use global option --noauth for anonymous calls here } for mod, msg in module.items(): - with mock.patch('koji_cli.commands.%s' % mod, new=None): + with mock.patch('koji_cli.commands.call.%s' % mod, new=None): with self.assertRaises(SystemExit) as ex: handle_call(self.options, self.session, arguments) self.assertExitCode(ex, 2) diff --git a/tests/test_cli/test_cancel.py b/tests/test_cli/test_cancel.py index 429ca74..89a931a 100644 --- a/tests/test_cli/test_cancel.py +++ b/tests/test_cli/test_cancel.py @@ -3,7 +3,7 @@ from __future__ import absolute_import import mock import koji -from koji_cli.commands import handle_cancel +from koji_cli.commands.cancel import handle_cancel from . import utils @@ -15,7 +15,7 @@ class TestCancel(utils.CliTestCase): self.options.quiet = False self.session = mock.MagicMock() self.session.multicall.return_value.__enter__.return_value = self.session - self.activate_session_mock = mock.patch('koji_cli.commands.activate_session').start() + self.activate_session_mock = mock.patch('koji_cli.commands.cancel.activate_session').start() self.error_format = """Usage: %s cancel [options] [ ...] (Specify the --help global option for a list of other help options) diff --git a/tests/test_cli/test_chain_build.py b/tests/test_cli/test_chain_build.py index e0510f6..3b56058 100644 --- a/tests/test_cli/test_chain_build.py +++ b/tests/test_cli/test_chain_build.py @@ -4,7 +4,7 @@ import mock import six import unittest -from koji_cli.commands import handle_chain_build +from koji_cli.commands.chain_build import handle_chain_build from . import utils @@ -20,10 +20,10 @@ class TestChainBuild(utils.CliTestCase): self.options.poll_interval = 0 # Mock out the xmlrpc server self.session = mock.MagicMock() - self.activate_session_mock = mock.patch('koji_cli.commands.activate_session').start() - self.running_in_bg_mock = mock.patch('koji_cli.commands._running_in_bg').start() + self.activate_session_mock = mock.patch('koji_cli.commands.chain_build.activate_session').start() + self.running_in_bg_mock = mock.patch('koji_cli.commands.chain_build._running_in_bg').start() self.running_in_bg_mock.return_value = False - self.watch_tasks_mock = mock.patch('koji_cli.commands.watch_tasks').start() + self.watch_tasks_mock = mock.patch('koji_cli.commands.chain_build.watch_tasks').start() self.watch_tasks_mock.return_value = 0 self.error_format = """Usage: %s chain-build [options] [ [:] [:] ...] (Specify the --help global option for a list of other help options) diff --git a/tests/test_cli/test_clone_tag.py b/tests/test_cli/test_clone_tag.py index 72a8684..b445553 100644 --- a/tests/test_cli/test_clone_tag.py +++ b/tests/test_cli/test_clone_tag.py @@ -7,7 +7,7 @@ from mock import call import unittest import koji -from koji_cli.commands import handle_clone_tag +from koji_cli.commands.clone_tag import handle_clone_tag from . import utils @@ -23,7 +23,7 @@ class TestCloneTag(utils.CliTestCase): 'locked': False}, {'id': 2, 'locked': False}] - self.activate_session = mock.patch('koji_cli.commands.activate_session').start() + self.activate_session = mock.patch('koji_cli.commands.clone_tag.activate_session').start() self.error_format = """Usage: %s clone-tag [options] clone-tag will create the destination tag if it does not already exist diff --git a/tests/test_cli/test_disable_channel.py b/tests/test_cli/test_disable_channel.py index a0aeaf7..44c7fde 100644 --- a/tests/test_cli/test_disable_channel.py +++ b/tests/test_cli/test_disable_channel.py @@ -6,7 +6,7 @@ import mock import six import koji -from koji_cli.commands import handle_disable_channel +from koji_cli.commands.disable_channel import handle_disable_channel from . import utils @@ -18,7 +18,7 @@ class TestDisableChannel(utils.CliTestCase): self.options = mock.MagicMock() self.options.quiet = False self.session = mock.MagicMock() - self.activate_session_mock = mock.patch('koji_cli.commands.activate_session').start() + self.activate_session_mock = mock.patch('koji_cli.commands.disable_channel.activate_session').start() self.error_format = """Usage: %s disable-channel [options] [ ...] (Specify the --help global option for a list of other help options) diff --git a/tests/test_cli/test_disable_host.py b/tests/test_cli/test_disable_host.py index 1dbc154..7ea6b81 100644 --- a/tests/test_cli/test_disable_host.py +++ b/tests/test_cli/test_disable_host.py @@ -4,7 +4,7 @@ import six import unittest from mock import call -from koji_cli.commands import handle_disable_host +from koji_cli.commands.disable_host import handle_disable_host from . import utils @@ -14,7 +14,7 @@ class TestDisableHost(utils.CliTestCase): self.options = mock.MagicMock() self.options.quiet = False self.session = mock.MagicMock() - self.activate_session_mock = mock.patch('koji_cli.commands.activate_session').start() + self.activate_session_mock = mock.patch('koji_cli.commands.disable_host.activate_session').start() self.error_format = """Usage: %s disable-host [options] [ ...] (Specify the --help global option for a list of other help options) diff --git a/tests/test_cli/test_disable_user.py b/tests/test_cli/test_disable_user.py index ff54eec..856c32c 100644 --- a/tests/test_cli/test_disable_user.py +++ b/tests/test_cli/test_disable_user.py @@ -3,7 +3,7 @@ import mock import six import unittest -from koji_cli.commands import handle_disable_user +from koji_cli.commands.disable_user import handle_disable_user from . import utils @@ -16,7 +16,7 @@ class TestDisableUser(utils.CliTestCase): self.options = mock.MagicMock() self.options.quiet = False self.session = mock.MagicMock() - self.activate_session_mock = mock.patch('koji_cli.commands.activate_session').start() + self.activate_session_mock = mock.patch('koji_cli.commands.disable_user.activate_session').start() self.username = 'user' self.error_format = """Usage: %s disable-user (Specify the --help global option for a list of other help options) diff --git a/tests/test_cli/test_dist_repo.py b/tests/test_cli/test_dist_repo.py index 48f3ca2..ee4db97 100644 --- a/tests/test_cli/test_dist_repo.py +++ b/tests/test_cli/test_dist_repo.py @@ -7,7 +7,7 @@ import copy import mock import six -from koji_cli.commands import handle_dist_repo +from koji_cli.commands.dist_repo import handle_dist_repo from . import utils @@ -80,14 +80,14 @@ via 'koji edit-tag -x distrepo.cancel_others=True' self.setUpMocks() def setUpMocks(self): - self.unique_path = mock.patch('koji_cli.commands.unique_path').start() + self.unique_path = mock.patch('koji_cli.commands.dist_repo.unique_path').start() self.unique_path.return_value = '/path/to/cli-dist-repo' - self.activate_session = mock.patch('koji_cli.commands.activate_session').start() + self.activate_session = mock.patch('koji_cli.commands.dist_repo.activate_session').start() - self.running_in_bg = mock.patch('koji_cli.commands._running_in_bg').start() + self.running_in_bg = mock.patch('koji_cli.commands.dist_repo._running_in_bg').start() self.running_in_bg.return_value = False # assume run in foreground - self.watch_tasks = mock.patch('koji_cli.commands.watch_tasks').start() + self.watch_tasks = mock.patch('koji_cli.commands.dist_repo.watch_tasks').start() self.watch_tasks.return_value = True self.mocks_table = {} diff --git a/tests/test_cli/test_download_logs.py b/tests/test_cli/test_download_logs.py index c25e583..ed0f2f1 100644 --- a/tests/test_cli/test_download_logs.py +++ b/tests/test_cli/test_download_logs.py @@ -3,7 +3,7 @@ from __future__ import absolute_import import mock import six -from koji_cli.commands import anon_handle_download_logs +from koji_cli.commands.download_logs import anon_handle_download_logs from . import utils @@ -21,10 +21,10 @@ class TestDownloadLogs(utils.CliTestCase): # Mock out the xmlrpc server self.session = mock.MagicMock() self.list_task_output_all_volumes = mock.patch( - 'koji_cli.commands.list_task_output_all_volumes').start() + 'koji_cli.commands.download_logs.list_task_output_all_volumes').start() self.ensuredir = mock.patch('koji.ensuredir').start() - self.download_file = mock.patch('koji_cli.commands.download_file').start() - self.ensure_connection = mock.patch('koji_cli.commands.ensure_connection').start() + self.download_file = mock.patch('koji_cli.commands.download_logs.download_file').start() + self.ensure_connection = mock.patch('koji_cli.commands.download_logs.ensure_connection').start() self.stdout = mock.patch('sys.stdout', new_callable=six.StringIO).start() self.error_format = """Usage: %s download-logs [options] [ ...] %s download-logs [options] --nvr [ ...] diff --git a/tests/test_cli/test_download_task.py b/tests/test_cli/test_download_task.py index e4d0362..a1b765c 100644 --- a/tests/test_cli/test_download_task.py +++ b/tests/test_cli/test_download_task.py @@ -7,7 +7,7 @@ import mock import six from mock import call -from koji_cli.commands import anon_handle_download_task +from koji_cli.commands.download_task import anon_handle_download_task from . import utils progname = os.path.basename(sys.argv[0]) or 'koji' @@ -47,10 +47,10 @@ class TestDownloadTask(utils.CliTestCase): # Mock out the xmlrpc server self.session = mock.MagicMock() self.list_task_output_all_volumes = mock.patch( - 'koji_cli.commands.list_task_output_all_volumes').start() + 'koji_cli.commands.download_task.list_task_output_all_volumes').start() self.ensuredir = mock.patch('koji.ensuredir').start() - self.download_file = mock.patch('koji_cli.commands.download_file').start() - self.ensure_connection = mock.patch('koji_cli.commands.ensure_connection').start() + self.download_file = mock.patch('koji_cli.commands.download_task.download_file').start() + self.ensure_connection = mock.patch('koji_cli.commands.download_task.ensure_connection').start() self.stdout = mock.patch('sys.stdout', new_callable=six.StringIO).start() self.stderr = mock.patch('sys.stderr', new_callable=six.StringIO).start() diff --git a/tests/test_cli/test_edit_channel.py b/tests/test_cli/test_edit_channel.py index 55f391e..bd4883e 100644 --- a/tests/test_cli/test_edit_channel.py +++ b/tests/test_cli/test_edit_channel.py @@ -6,7 +6,7 @@ import unittest import mock import koji -from koji_cli.commands import handle_edit_channel +from koji_cli.commands.edit_channel import handle_edit_channel from . import utils @@ -24,7 +24,7 @@ class TestEditChannel(utils.CliTestCase): 'description': self.description, } self.maxDiff = None - self.activate_session_mock = mock.patch('koji_cli.commands.activate_session').start() + self.activate_session_mock = mock.patch('koji_cli.commands.edit_channel.activate_session').start() self.error_format = """Usage: %s edit-channel [options] (Specify the --help global option for a list of other help options) diff --git a/tests/test_cli/test_edit_external_repo.py b/tests/test_cli/test_edit_external_repo.py index 10a8f1e..bd87404 100644 --- a/tests/test_cli/test_edit_external_repo.py +++ b/tests/test_cli/test_edit_external_repo.py @@ -5,7 +5,7 @@ import six import unittest import koji -from koji_cli.commands import handle_edit_external_repo +from koji_cli.commands.edit_external_repo import handle_edit_external_repo from . import utils @@ -17,7 +17,8 @@ class TestEditExternalRepo(utils.CliTestCase): def setUp(self): self.options = mock.MagicMock() self.session = mock.MagicMock() - self.activate_session_mock = mock.patch('koji_cli.commands.activate_session').start() + self.activate_session_mock = \ + mock.patch('koji_cli.commands.edit_external_repo.activate_session').start() self.error_format = """Usage: %s edit-external-repo [options] (Specify the --help global option for a list of other help options) diff --git a/tests/test_cli/test_edit_host.py b/tests/test_cli/test_edit_host.py index ce98e9c..1f702cd 100644 --- a/tests/test_cli/test_edit_host.py +++ b/tests/test_cli/test_edit_host.py @@ -6,7 +6,7 @@ import koji from mock import call -from koji_cli.commands import handle_edit_host +from koji_cli.commands.edit_host import handle_edit_host from . import utils @@ -16,7 +16,7 @@ class TestEditHost(utils.CliTestCase): self.options.debug = False self.session = mock.MagicMock() self.session.getAPIVersion.return_value = koji.API_VERSION - self.activate_session_mock = mock.patch('koji_cli.commands.activate_session').start() + self.activate_session_mock = mock.patch('koji_cli.commands.edit_host.activate_session').start() self.error_format = """Usage: %s edit-host [ ...] [options] (Specify the --help global option for a list of other help options) diff --git a/tests/test_cli/test_edit_notification.py b/tests/test_cli/test_edit_notification.py index 21b8cc0..795d9ee 100644 --- a/tests/test_cli/test_edit_notification.py +++ b/tests/test_cli/test_edit_notification.py @@ -2,7 +2,7 @@ from __future__ import absolute_import import koji import mock -from koji_cli.commands import handle_edit_notification +from koji_cli.commands.edit_notification import handle_edit_notification from . import utils @@ -12,7 +12,8 @@ class TestEditNotification(utils.CliTestCase): self.options.debug = False self.session = mock.MagicMock() self.session.getAPIVersion.return_value = koji.API_VERSION - self.activate_session_mock = mock.patch('koji_cli.commands.activate_session').start() + self.activate_session_mock = \ + mock.patch('koji_cli.commands.edit_notification.activate_session').start() self.error_format = """Usage: %s edit-notification [options] (Specify the --help global option for a list of other help options) diff --git a/tests/test_cli/test_edit_permission.py b/tests/test_cli/test_edit_permission.py index 82c0e8e..562ba3a 100644 --- a/tests/test_cli/test_edit_permission.py +++ b/tests/test_cli/test_edit_permission.py @@ -3,7 +3,7 @@ import mock import koji import six -from koji_cli.commands import handle_edit_permission +from koji_cli.commands.edit_permission import handle_edit_permission from . import utils @@ -17,7 +17,8 @@ class TestEditPermission(utils.CliTestCase): self.options.debug = False self.session = mock.MagicMock() self.session.getAPIVersion.return_value = koji.API_VERSION - self.activate_session_mock = mock.patch('koji_cli.commands.activate_session').start() + self.activate_session_mock = \ + mock.patch('koji_cli.commands.edit_permission.activate_session').start() self.error_format = """Usage: %s edit-permission (Specify the --help global option for a list of other help options) diff --git a/tests/test_cli/test_edit_tag.py b/tests/test_cli/test_edit_tag.py index 19e302d..b3c0746 100644 --- a/tests/test_cli/test_edit_tag.py +++ b/tests/test_cli/test_edit_tag.py @@ -3,7 +3,7 @@ import mock import six import koji -from koji_cli.commands import handle_edit_tag +from koji_cli.commands.edit_tag import handle_edit_tag from . import utils @@ -14,7 +14,7 @@ class TestEditTag(utils.CliTestCase): self.options.debug = False self.session = mock.MagicMock() self.session.getAPIVersion.return_value = koji.API_VERSION - self.activate_session_mock = mock.patch('koji_cli.commands.activate_session').start() + self.activate_session_mock = mock.patch('koji_cli.commands.edit_tag.activate_session').start() self.error_format = """Usage: %s edit-tag [options] (Specify the --help global option for a list of other help options) diff --git a/tests/test_cli/test_edit_tag_inheritance.py b/tests/test_cli/test_edit_tag_inheritance.py index c2e345f..4f790df 100644 --- a/tests/test_cli/test_edit_tag_inheritance.py +++ b/tests/test_cli/test_edit_tag_inheritance.py @@ -4,7 +4,7 @@ import mock from mock import call import koji -from koji_cli.commands import handle_edit_tag_inheritance +from koji_cli.commands.edit_tag_inheritance import handle_edit_tag_inheritance from . import utils @@ -14,7 +14,7 @@ class TestEditTagInheritance(utils.CliTestCase): self.options.debug = False self.session = mock.MagicMock() self.session.getAPIVersion.return_value = koji.API_VERSION - self.activate_session_mock = mock.patch('koji_cli.commands.activate_session').start() + self.activate_session_mock = mock.patch('koji_cli.commands.edit_tag_inheritance.activate_session').start() self.error_format = """Usage: %s edit-tag-inheritance [options] (Specify the --help global option for a list of other help options) diff --git a/tests/test_cli/test_edit_target.py b/tests/test_cli/test_edit_target.py index d3d5557..5346d07 100644 --- a/tests/test_cli/test_edit_target.py +++ b/tests/test_cli/test_edit_target.py @@ -6,7 +6,7 @@ import mock from six.moves import StringIO import koji -from koji_cli.commands import handle_edit_target +from koji_cli.commands.edit_target import handle_edit_target from . import utils @@ -16,7 +16,7 @@ class TestEditTarget(utils.CliTestCase): self.options.debug = False self.session = mock.MagicMock() self.session.getAPIVersion.return_value = koji.API_VERSION - self.activate_session_mock = mock.patch('koji_cli.commands.activate_session').start() + self.activate_session_mock = mock.patch('koji_cli.commands.edit_target.activate_session').start() self.error_format = """Usage: %s edit-target [options] (Specify the --help global option for a list of other help options) diff --git a/tests/test_cli/test_edit_user.py b/tests/test_cli/test_edit_user.py index 0b5784a..b4d47d1 100644 --- a/tests/test_cli/test_edit_user.py +++ b/tests/test_cli/test_edit_user.py @@ -7,7 +7,7 @@ import mock import koji -from koji_cli.commands import handle_edit_user +from koji_cli.commands.edit_user import handle_edit_user from . import utils @@ -17,7 +17,7 @@ class TestEditUser(utils.CliTestCase): self.options.debug = False self.session = mock.MagicMock() self.session.getAPIVersion.return_value = koji.API_VERSION - self.activate_session_mock = mock.patch('koji_cli.commands.activate_session').start() + self.activate_session_mock = mock.patch('koji_cli.commands.edit_user.activate_session').start() self.error_format = """Usage: %s edit-user [options] (Specify the --help global option for a list of other help options) diff --git a/tests/test_cli/test_enable_channel.py b/tests/test_cli/test_enable_channel.py index 2c985e2..f2af371 100644 --- a/tests/test_cli/test_enable_channel.py +++ b/tests/test_cli/test_enable_channel.py @@ -6,7 +6,7 @@ import mock import six import koji -from koji_cli.commands import handle_enable_channel +from koji_cli.commands.enable_channel import handle_enable_channel from . import utils @@ -19,7 +19,7 @@ class TestEnableChannel(utils.CliTestCase): self.options.debug = False self.session = mock.MagicMock() self.session.getAPIVersion.return_value = koji.API_VERSION - self.activate_session_mock = mock.patch('koji_cli.commands.activate_session').start() + self.activate_session_mock = mock.patch('koji_cli.commands.enable_channel.activate_session').start() self.error_format = """Usage: %s enable-channel [options] [ ...] (Specify the --help global option for a list of other help options) diff --git a/tests/test_cli/test_enable_host.py b/tests/test_cli/test_enable_host.py index 276f692..a4db2a9 100644 --- a/tests/test_cli/test_enable_host.py +++ b/tests/test_cli/test_enable_host.py @@ -5,7 +5,7 @@ import unittest import koji from mock import call -from koji_cli.commands import handle_enable_host +from koji_cli.commands.enable_host import handle_enable_host from . import utils @@ -15,7 +15,7 @@ class TestEnableHost(utils.CliTestCase): self.options.debug = False self.session = mock.MagicMock() self.session.getAPIVersion.return_value = koji.API_VERSION - self.activate_session_mock = mock.patch('koji_cli.commands.activate_session').start() + self.activate_session_mock = mock.patch('koji_cli.commands.enable_host.activate_session').start() self.error_format = """Usage: %s enable-host [options] [ ...] (Specify the --help global option for a list of other help options) diff --git a/tests/test_cli/test_enable_user.py b/tests/test_cli/test_enable_user.py index 58ce95c..d72f866 100644 --- a/tests/test_cli/test_enable_user.py +++ b/tests/test_cli/test_enable_user.py @@ -3,7 +3,7 @@ import mock import unittest import koji -from koji_cli.commands import handle_enable_user +from koji_cli.commands.enable_user import handle_enable_user from . import utils @@ -13,7 +13,7 @@ class TestEnableUser(utils.CliTestCase): self.options.debug = False self.session = mock.MagicMock() self.session.getAPIVersion.return_value = koji.API_VERSION - self.activate_session_mock = mock.patch('koji_cli.commands.activate_session').start() + self.activate_session_mock = mock.patch('koji_cli.commands.enable_user.activate_session').start() self.error_format = """Usage: %s enable-user (Specify the --help global option for a list of other help options) diff --git a/tests/test_cli/test_free_task.py b/tests/test_cli/test_free_task.py index 705afb6..2b0206f 100644 --- a/tests/test_cli/test_free_task.py +++ b/tests/test_cli/test_free_task.py @@ -2,7 +2,7 @@ from __future__ import absolute_import import mock from six.moves import StringIO -from koji_cli.commands import handle_free_task +from koji_cli.commands.free_task import handle_free_task from . import utils @@ -12,7 +12,7 @@ class TestFreeTask(utils.CliTestCase): self.options = mock.MagicMock() self.maxDiff = None self.session = mock.MagicMock() - self.activate_session_mock = mock.patch('koji_cli.commands.activate_session').start() + self.activate_session_mock = mock.patch('koji_cli.commands.free_task.activate_session').start() self.error_format = """Usage: %s free-task [options] [ ...] (Specify the --help global option for a list of other help options) diff --git a/tests/test_cli/test_grant_cg_access.py b/tests/test_cli/test_grant_cg_access.py index ed71eb3..040cbaf 100644 --- a/tests/test_cli/test_grant_cg_access.py +++ b/tests/test_cli/test_grant_cg_access.py @@ -2,7 +2,7 @@ from __future__ import absolute_import import mock import unittest -from koji_cli.commands import handle_grant_cg_access +from koji_cli.commands.grant_cg_access import handle_grant_cg_access from . import utils @@ -18,7 +18,7 @@ class TestGrantCGAccess(utils.CliTestCase): self.options = mock.MagicMock() self.options.quiet = False self.session = mock.MagicMock() - self.activate_session_mock = mock.patch('koji_cli.commands.activate_session').start() + self.activate_session_mock = mock.patch('koji_cli.commands.grant_cg_access.activate_session').start() self.cg = 'cg' self.user = 'user' diff --git a/tests/test_cli/test_grant_permission.py b/tests/test_cli/test_grant_permission.py index d70e9aa..62881d5 100644 --- a/tests/test_cli/test_grant_permission.py +++ b/tests/test_cli/test_grant_permission.py @@ -3,7 +3,7 @@ import mock import unittest import koji -from koji_cli.commands import handle_grant_permission +from koji_cli.commands.grant_permission import handle_grant_permission from . import utils @@ -17,7 +17,7 @@ class TestGrantPermission(utils.CliTestCase): self.options.debug = False self.session = mock.MagicMock() self.session.getAPIVersion.return_value = koji.API_VERSION - self.activate_session_mock = mock.patch('koji_cli.commands.activate_session').start() + self.activate_session_mock = mock.patch('koji_cli.commands.grant_permission.activate_session').start() self.error_format = """Usage: %s grant-permission [options] [ ...] (Specify the --help global option for a list of other help options) diff --git a/tests/test_cli/test_hello.py b/tests/test_cli/test_hello.py index a054b9b..083487f 100644 --- a/tests/test_cli/test_hello.py +++ b/tests/test_cli/test_hello.py @@ -5,7 +5,7 @@ import six import unittest import koji -from koji_cli.commands import handle_moshimoshi, _printable_unicode +from koji_cli.commands.moshimoshi import handle_moshimoshi, _printable_unicode from . import utils @@ -31,7 +31,7 @@ class TestHello(utils.CliTestCase): self.maxDiff = None self.options = mock.MagicMock() self.options.quiet = False - self.activate_session_mock = mock.patch('koji_cli.commands.activate_session').start() + self.activate_session_mock = mock.patch('koji_cli.commands.moshimoshi.activate_session').start() self.error_format = """Usage: %s moshimoshi [options] (Specify the --help global option for a list of other help options) @@ -40,7 +40,7 @@ class TestHello(utils.CliTestCase): self.huburl = "https://%s.local/%shub" % (self.progname, self.progname) @mock.patch('sys.stdout', new_callable=six.StringIO) - @mock.patch('koji_cli.commands._printable_unicode') + @mock.patch('koji_cli.commands.moshimoshi._printable_unicode') def test_handle_moshimoshi(self, print_unicode_mock, stdout): """Test handle_moshimoshi function""" user = {'name': self.progname, diff --git a/tests/test_cli/test_hostinfo.py b/tests/test_cli/test_hostinfo.py index 37dabf2..b2cb5db 100644 --- a/tests/test_cli/test_hostinfo.py +++ b/tests/test_cli/test_hostinfo.py @@ -7,7 +7,7 @@ from six.moves import StringIO import copy import koji -from koji_cli.commands import anon_handle_hostinfo +from koji_cli.commands.hostinfo import anon_handle_hostinfo from . import utils @@ -40,7 +40,7 @@ class TestHostinfo(utils.CliTestCase): %s: error: {message} """ % (self.progname, self.progname) - self.ensure_connection_mock = mock.patch('koji_cli.commands.ensure_connection').start() + self.ensure_connection_mock = mock.patch('koji_cli.commands.hostinfo.ensure_connection').start() def tearDown(self): locale.resetlocale() diff --git a/tests/test_cli/test_image_build.py b/tests/test_cli/test_image_build.py index 42e2e56..0fdf373 100644 --- a/tests/test_cli/test_image_build.py +++ b/tests/test_cli/test_image_build.py @@ -6,7 +6,7 @@ import unittest import koji -from koji_cli.commands import handle_image_build, _build_image_oz +from koji_cli.commands.image_build import handle_image_build, _build_image_oz from . import utils if six.PY2: @@ -110,11 +110,11 @@ class TestBuildImageOz(utils.CliTestCase): } # mocks - self.activate_session = mock.patch('koji_cli.commands.activate_session').start() - self.watch_tasks = mock.patch('koji_cli.commands.watch_tasks').start() - self.unique_path = mock.patch('koji_cli.commands.unique_path').start() + self.activate_session = mock.patch('koji_cli.commands.image_build.activate_session').start() + self.watch_tasks = mock.patch('koji_cli.commands.image_build.watch_tasks').start() + self.unique_path = mock.patch('koji_cli.commands.image_build.unique_path').start() self.unique_path.return_value = '/path/to/cli-image' - self.running_in_bg = mock.patch('koji_cli.commands._running_in_bg').start() + self.running_in_bg = mock.patch('koji_cli.commands.image_build._running_in_bg').start() self.running_in_bg.return_value = False def tearDown(self): @@ -250,7 +250,7 @@ class TestImageBuild(utils.CliTestCase): exit_code=2) self.session.activate_session_mock.assert_not_called() - @mock.patch('koji_cli.commands._build_image_oz') + @mock.patch('koji_cli.commands.image_build._build_image_oz') def test_handle_image_build_with_config_valid_with_config(self, build_image_oz_mock): """Test handle_image_build.""" config_file = os.path.join(os.path.dirname(__file__), 'data/image-build-config.conf') diff --git a/tests/test_cli/test_image_build_indirection.py b/tests/test_cli/test_image_build_indirection.py index 0610c0f..c8db80b 100644 --- a/tests/test_cli/test_image_build_indirection.py +++ b/tests/test_cli/test_image_build_indirection.py @@ -5,7 +5,8 @@ import unittest import koji -from koji_cli.commands import handle_image_build_indirection, _build_image_indirection +from koji_cli.commands.image_build_indirection import (handle_image_build_indirection, + _build_image_indirection) from . import utils @@ -46,8 +47,8 @@ class TestBuildImageIndirection(utils.CliTestCase): self.options.quiet = False self.options.weburl = self.weburl self.session = mock.MagicMock() - self.activate_session = mock.patch('koji_cli.commands.activate_session').start() - self.unique_path = mock.patch('koji_cli.commands.unique_path').start() + self.activate_session = mock.patch('koji_cli.commands.image_build_indirection.activate_session').start() + self.unique_path = mock.patch('koji_cli.commands.image_build_indirection.unique_path').start() self.task_opts = Options(TASK_OPTIONS) @@ -198,7 +199,7 @@ class TestImageBuildIndirection(utils.CliTestCase): %s: error: {message} """ % (self.progname, self.progname, self.progname) - @mock.patch('koji_cli.commands._build_image_indirection') + @mock.patch('koji_cli.commands.image_build_indirection._build_image_indirection') def test_handle_image_build_indirection(self, build_image_mock): """Test handle_image_build_indirection function""" handle_image_build_indirection(self.options, self.session, []) diff --git a/tests/test_cli/test_import.py b/tests/test_cli/test_import.py index e91a5a7..f6f7a88 100644 --- a/tests/test_cli/test_import.py +++ b/tests/test_cli/test_import.py @@ -5,7 +5,7 @@ import six import unittest import koji -from koji_cli.commands import handle_import +from koji_cli.commands.cmd_import import handle_import from . import utils @@ -16,7 +16,7 @@ def md5_to_bytes(s): return bytearray.fromhex(s) -class TestImport(utils.CliTestCase): +class TestCmdImport(utils.CliTestCase): def setUp(self): self.maxDiff = None self.huburl = "https://%s.local/%shub" % (self.progname, self.progname) @@ -91,8 +91,8 @@ class TestImport(utils.CliTestCase): upload_rpm_mock = kwargs.get('upload_rpm_mock', session.uploadWrapper) with mock.patch('koji.get_header_fields') as get_header_fields_mock: - with mock.patch('koji_cli.commands.unique_path') as unique_path_mock: - with mock.patch('koji_cli.commands.activate_session') as activate_session_mock: + with mock.patch('koji_cli.commands.cmd_import.unique_path') as unique_path_mock: + with mock.patch('koji_cli.commands.cmd_import.activate_session') as activate_session_mock: with mock.patch('sys.stdout', new_callable=six.StringIO) as stdout: with upload_rpm_mock: get_header_fields_mock.return_value = rpm_header @@ -130,7 +130,7 @@ class TestImport(utils.CliTestCase): @mock.patch('sys.stdout', new_callable=six.StringIO) @mock.patch('sys.stderr', new_callable=six.StringIO) - @mock.patch('koji_cli.commands.activate_session') + @mock.patch('koji_cli.commands.cmd_import.activate_session') def __skip_import_test(self, options, session, arguments, activate_session_mock, stderr, stdout, **kwargs): expected = kwargs.get('expected', None) @@ -194,7 +194,7 @@ class TestImport(utils.CliTestCase): expected = "importing %s... done\n" % arguments[0] self.__do_import_test( options, session, arguments + ['--link'], - upload_rpm_mock=mock.patch('koji_cli.commands.linked_upload').start(), + upload_rpm_mock=mock.patch('koji_cli.commands.cmd_import.linked_upload').start(), rpm_header=self.srpm_header, expected=expected) session.uploadWrapper.assert_not_called() @@ -309,7 +309,7 @@ class TestImport(utils.CliTestCase): } expected = "Build %s state is %s. Skipping import\n" % (nvr, 'FAILED') with mock.patch('koji.get_header_fields') as get_header_fields_mock: - with mock.patch('koji_cli.commands.activate_session') as activate_session_mock: + with mock.patch('koji_cli.commands.cmd_import.activate_session') as activate_session_mock: with mock.patch('sys.stdout', new_callable=six.StringIO) as stdout: get_header_fields_mock.return_value = self.srpm_header handle_import(options, session, arguments) @@ -352,7 +352,7 @@ class TestImport(utils.CliTestCase): @mock.patch('sys.stdout', new_callable=six.StringIO) @mock.patch('sys.stderr', new_callable=six.StringIO) @mock.patch('koji.get_header_fields') - @mock.patch('koji_cli.commands.activate_session') + @mock.patch('koji_cli.commands.cmd_import.activate_session') def test_handle_import_binary_rpm_import_with_no_build_exist( self, activate_session_mock, @@ -398,7 +398,7 @@ class TestImport(utils.CliTestCase): @mock.patch('sys.stdout', new_callable=six.StringIO) @mock.patch('sys.stderr', new_callable=six.StringIO) - @mock.patch('koji_cli.commands.activate_session') + @mock.patch('koji_cli.commands.cmd_import.activate_session') def test_handle_import_binary_rpm_with_exist_build( self, activate_session_mock, @@ -475,8 +475,8 @@ class TestImport(utils.CliTestCase): @mock.patch('sys.stdout', new_callable=six.StringIO) @mock.patch('koji.get_header_fields') - @mock.patch('koji_cli.commands.unique_path') - @mock.patch('koji_cli.commands.activate_session') + @mock.patch('koji_cli.commands.cmd_import.unique_path') + @mock.patch('koji_cli.commands.cmd_import.activate_session') def test_handle_import_with_test_option( self, activate_session_mock, @@ -524,7 +524,7 @@ class TestImport(utils.CliTestCase): expected = "Test mode -- would have created empty build: %s\n" % nvr() expected += "Test mode -- skipping import for %s\n" % arguments[0] - with mock.patch('koji_cli.commands.unique_path') as unique_path_mock: + with mock.patch('koji_cli.commands.cmd_import.unique_path') as unique_path_mock: handle_import(options, session, arguments) self.assert_console_message(stdout, expected) unique_path_mock.assert_not_called() @@ -533,8 +533,8 @@ class TestImport(utils.CliTestCase): @mock.patch('sys.stdout', new_callable=six.StringIO) @mock.patch('koji.get_header_fields') - @mock.patch('koji_cli.commands.unique_path') - @mock.patch('koji_cli.commands.activate_session') + @mock.patch('koji_cli.commands.cmd_import.unique_path') + @mock.patch('koji_cli.commands.cmd_import.activate_session') def test_handle_import_with_epoch_option( self, activate_session_mock, @@ -590,7 +590,7 @@ class TestImport(utils.CliTestCase): @mock.patch('sys.stdout', new_callable=six.StringIO) @mock.patch('sys.stderr', new_callable=six.StringIO) @mock.patch('koji.get_header_fields') - @mock.patch('koji_cli.commands.activate_session') + @mock.patch('koji_cli.commands.cmd_import.activate_session') def test_handle_import_has_build_states_test( self, activate_session_mock, @@ -643,7 +643,7 @@ class TestImport(utils.CliTestCase): session.importRPM.assert_not_called() @mock.patch('sys.stderr', new_callable=six.StringIO) - @mock.patch('koji_cli.commands.activate_session') + @mock.patch('koji_cli.commands.cmd_import.activate_session') def test_handle_import_argument_error( self, activate_session_mock, diff --git a/tests/test_cli/test_import_archive.py b/tests/test_cli/test_import_archive.py index 9887df2..639e746 100644 --- a/tests/test_cli/test_import_archive.py +++ b/tests/test_cli/test_import_archive.py @@ -3,7 +3,7 @@ from __future__ import absolute_import import mock import koji -from koji_cli.commands import handle_import_archive +from koji_cli.commands.import_archive import handle_import_archive from . import utils @@ -16,7 +16,7 @@ class TestImportArchive(utils.CliTestCase): self.session.getAPIVersion.return_value = koji.API_VERSION self.build_id = '1' self.archive_path = '/mnt/brew/work/test-archive.type' - self.activate_session_mock = mock.patch('koji_cli.commands.activate_session').start() + self.activate_session_mock = mock.patch('koji_cli.commands.import_archive.activate_session').start() self.error_format = """Usage: %s import-archive [ (Specify the --help global option for a list of other help options) @@ -35,7 +35,7 @@ class TestImportCG(utils.CliTestCase): """ % (self.progname, self.progname) @mock.patch('sys.stdout', new_callable=six.StringIO) - @mock.patch('koji_cli.commands._progress_callback') + @mock.patch('koji_cli.commands.import_cg._progress_callback') @mock.patch('koji.json') def test_handle_import_cg(self, json_mock, progress_callback_mock, stdout): """Test handle_import_cg function""" diff --git a/tests/test_cli/test_import_comps.py b/tests/test_cli/test_import_comps.py index d28d6d2..458ebce 100644 --- a/tests/test_cli/test_import_comps.py +++ b/tests/test_cli/test_import_comps.py @@ -19,8 +19,8 @@ try: except ImportError: yumcomps = None -import koji_cli.commands -from koji_cli.commands import handle_import_comps, _import_comps, _import_comps_alt +import koji_cli.commands.import_comps +from koji_cli.commands.import_comps import handle_import_comps, _import_comps, _import_comps_alt from . import utils @@ -31,7 +31,7 @@ class TestImportComps(utils.CliTestCase): self.options.debug = False self.session = mock.MagicMock() self.session.getAPIVersion.return_value = koji.API_VERSION - self.mock_activate_session = mock.patch('koji_cli.commands.activate_session').start() + self.mock_activate_session = mock.patch('koji_cli.commands.import_comps.activate_session').start() self.error_format = """Usage: %s import-comps [options] (Specify the --help global option for a list of other help options) @@ -39,9 +39,9 @@ class TestImportComps(utils.CliTestCase): """ % (self.progname, self.progname) @mock.patch('sys.stdout', new_callable=six.StringIO) - @mock.patch('koji_cli.commands.libcomps') - @mock.patch('koji_cli.commands._import_comps') - @mock.patch('koji_cli.commands._import_comps_alt') + @mock.patch('koji_cli.commands.import_comps.libcomps') + @mock.patch('koji_cli.commands.import_comps._import_comps') + @mock.patch('koji_cli.commands.import_comps._import_comps_alt') def test_handle_import_comps_libcomps( self, mock_import_comps_alt, @@ -73,10 +73,10 @@ class TestImportComps(utils.CliTestCase): self.assertNotEqual(rv, 1) @mock.patch('sys.stdout', new_callable=six.StringIO) - @mock.patch('koji_cli.commands.libcomps', new=None) - @mock.patch('koji_cli.commands.yumcomps', create=True) - @mock.patch('koji_cli.commands._import_comps') - @mock.patch('koji_cli.commands._import_comps_alt') + @mock.patch('koji_cli.commands.import_comps.libcomps', new=None) + @mock.patch('koji_cli.commands.import_comps.yumcomps', create=True) + @mock.patch('koji_cli.commands.import_comps._import_comps') + @mock.patch('koji_cli.commands.import_comps._import_comps_alt') def test_handle_import_comps_yumcomps( self, mock_import_comps_alt, @@ -107,10 +107,10 @@ class TestImportComps(utils.CliTestCase): mock_import_comps_alt.assert_called_once_with(self.session, filename, tag, local_options) self.assertNotEqual(rv, 1) - @mock.patch('koji_cli.commands.libcomps', new=None) - @mock.patch('koji_cli.commands.yumcomps', new=None, create=True) - @mock.patch('koji_cli.commands._import_comps') - @mock.patch('koji_cli.commands._import_comps_alt') + @mock.patch('koji_cli.commands.import_comps.libcomps', new=None) + @mock.patch('koji_cli.commands.import_comps.yumcomps', new=None, create=True) + @mock.patch('koji_cli.commands.import_comps._import_comps') + @mock.patch('koji_cli.commands.import_comps._import_comps_alt') def test_handle_import_comps_comps_na(self, mock_import_comps_alt, mock_import_comps): filename = './data/comps-example.xml' tag = 'tag' @@ -135,8 +135,8 @@ class TestImportComps(utils.CliTestCase): mock_import_comps.assert_not_called() mock_import_comps_alt.assert_not_called() - @mock.patch('koji_cli.commands._import_comps') - @mock.patch('koji_cli.commands._import_comps_alt') + @mock.patch('koji_cli.commands.import_comps._import_comps') + @mock.patch('koji_cli.commands.import_comps._import_comps_alt') def test_handle_import_comps_tag_not_exists(self, mock_import_comps_alt, mock_import_comps): filename = './data/comps-example.xml' tag = 'tag' @@ -212,8 +212,8 @@ class TestImportComps(utils.CliTestCase): stdout) @mock.patch('sys.stdout', new_callable=six.StringIO) - @mock.patch('koji_cli.commands.libcomps', new=None) - @mock.patch('koji_cli.commands.yumcomps', create=True, new=yumcomps) + @mock.patch('koji_cli.commands.import_comps.libcomps', new=None) + @mock.patch('koji_cli.commands.import_comps.yumcomps', create=True, new=yumcomps) def _test_import_comps_yumcomps(self, stdout): if yumcomps is None: pytest.skip('no yum.comps') @@ -230,8 +230,8 @@ class TestImportComps(utils.CliTestCase): stdout) @mock.patch('sys.stdout', new_callable=six.StringIO) - @mock.patch('koji_cli.commands.libcomps', new=None) - @mock.patch('koji_cli.commands.yumcomps', create=True, new=yumcomps) + @mock.patch('koji_cli.commands.import_comps.libcomps', new=None) + @mock.patch('koji_cli.commands.import_comps.yumcomps', create=True, new=yumcomps) def _test_import_comps_sample_yumcomps(self, stdout): if yumcomps is None: pytest.skip('no yum.comps') @@ -314,7 +314,7 @@ def generate_out_calls(): calls_file = path + '/data/comps-sample.libcomps.calls' _generate_out_calls(_import_comps, comps_file, stdout_file, calls_file) - koji_cli.commands.yumcomps = yumcomps + koji_cli.commands.import_comps.yumcomps = yumcomps comps_file = path + '/data/comps-example.xml' stdout_file = path + '/data/comps-example.yumcomps.out' diff --git a/tests/test_cli/test_import_sig.py b/tests/test_cli/test_import_sig.py index 12041fc..9762cdc 100644 --- a/tests/test_cli/test_import_sig.py +++ b/tests/test_cli/test_import_sig.py @@ -9,7 +9,7 @@ import unittest from mock import call from koji.util import base64encode -from koji_cli.commands import handle_import_sig +from koji_cli.commands.import_sig import handle_import_sig from . import utils import os @@ -30,7 +30,8 @@ class TestImportSIG(utils.CliTestCase): self.maxDiff = None self.options = mock.MagicMock() self.session = mock.MagicMock() - self.activate_session_mock = mock.patch('koji_cli.commands.activate_session').start() + self.activate_session_mock = \ + mock.patch('koji_cli.commands.import_sig.activate_session').start() self.custom_os_path_exists = {} self.os_path_exists = os.path.exists diff --git a/tests/test_cli/test_latest_build.py b/tests/test_cli/test_latest_build.py index 73baa98..7a3e575 100644 --- a/tests/test_cli/test_latest_build.py +++ b/tests/test_cli/test_latest_build.py @@ -4,7 +4,7 @@ import unittest import mock -from koji_cli.commands import anon_handle_latest_build +from koji_cli.commands.latest_build import anon_handle_latest_build from . import utils @@ -14,8 +14,7 @@ class TestLatestBuild(utils.CliTestCase): self.maxDiff = None self.options = mock.MagicMock() self.session = mock.MagicMock() - self.activate_session_mock = mock.patch('koji_cli.commands.activate_session').start() - self.ensure_connection = mock.patch('koji_cli.commands.ensure_connection').start() + self.ensure_connection = mock.patch('koji_cli.commands.latest_build.ensure_connection').start() self.tag_name = 'test-tag' self.pkg_name = 'test-pkg' self.expected_part_help = """Usage: %s latest-build [options] [ ...] @@ -43,7 +42,6 @@ https://docs.pagure.org/koji/HOWTO/#package-organization stderr=self.expected_part_help + expected, exit_code=2, activate_session=None) - self.activate_session_mock.assert_not_called() self.ensure_connection.assert_not_called() def test_handle_latest_build_more_args(self): @@ -55,7 +53,6 @@ https://docs.pagure.org/koji/HOWTO/#package-organization stderr=self.expected_part_help + expected, exit_code=2, activate_session=None) - self.activate_session_mock.assert_not_called() self.ensure_connection.called_once() def test_handle_latest_build_all_and_pkg(self): @@ -67,7 +64,6 @@ https://docs.pagure.org/koji/HOWTO/#package-organization stderr=self.expected_part_help + expected, exit_code=2, activate_session=None) - self.activate_session_mock.assert_not_called() self.ensure_connection.called_once() def test_handle_latest_build_help(self): diff --git a/tests/test_cli/test_list_api.py b/tests/test_cli/test_list_api.py index 59606cf..986eb46 100644 --- a/tests/test_cli/test_list_api.py +++ b/tests/test_cli/test_list_api.py @@ -3,7 +3,7 @@ import mock import six import unittest -from koji_cli.commands import anon_handle_list_api +from koji_cli.commands.list_api import anon_handle_list_api from . import utils @@ -17,7 +17,7 @@ class TestListApi(utils.CliTestCase): """ % (self.progname, self.progname) self.session = mock.MagicMock() self.options = mock.MagicMock() - self.ensure_connection = mock.patch('koji_cli.commands.ensure_connection').start() + self.ensure_connection = mock.patch('koji_cli.commands.list_api.ensure_connection').start() @mock.patch('sys.stdout', new_callable=six.StringIO) def test_anon_handle_list_api_all_method(self, stdout): diff --git a/tests/test_cli/test_list_buildroot.py b/tests/test_cli/test_list_buildroot.py index 7519926..be3b8e2 100644 --- a/tests/test_cli/test_list_buildroot.py +++ b/tests/test_cli/test_list_buildroot.py @@ -4,7 +4,7 @@ import mock from six.moves import StringIO import koji -from koji_cli.commands import anon_handle_list_buildroot +from koji_cli.commands.list_buildroot import anon_handle_list_buildroot from . import utils @@ -15,7 +15,8 @@ class TestListBuilds(utils.CliTestCase): self.options.debug = False self.session = mock.MagicMock() self.session.getAPIVersion.return_value = koji.API_VERSION - self.ensure_connection_mock = mock.patch('koji_cli.commands.ensure_connection').start() + self.ensure_connection_mock = \ + mock.patch('koji_cli.commands.list_buildroot.ensure_connection').start() self.error_format = """Usage: %s list-buildroot [options] (Specify the --help global option for a list of other help options) diff --git a/tests/test_cli/test_list_builds.py b/tests/test_cli/test_list_builds.py index 068efe9..490a835 100644 --- a/tests/test_cli/test_list_builds.py +++ b/tests/test_cli/test_list_builds.py @@ -4,7 +4,7 @@ import mock from six.moves import StringIO import koji -from koji_cli.commands import anon_handle_list_builds +from koji_cli.commands.list_builds import anon_handle_list_builds from . import utils @@ -15,7 +15,7 @@ class TestListBuilds(utils.CliTestCase): self.options.debug = False self.session = mock.MagicMock() self.session.getAPIVersion.return_value = koji.API_VERSION - self.ensure_connection_mock = mock.patch('koji_cli.commands.ensure_connection').start() + self.ensure_connection_mock = mock.patch('koji_cli.commands.list_builds.ensure_connection').start() self.user_info = {'id': 1, 'name': 'kojiadmin', 'status': 0, 'usertype': 0, 'krb_principals': []} self.owner = 'kojiadmin' diff --git a/tests/test_cli/test_list_channels.py b/tests/test_cli/test_list_channels.py index 1ac30fc..c1c9ab1 100644 --- a/tests/test_cli/test_list_channels.py +++ b/tests/test_cli/test_list_channels.py @@ -6,7 +6,7 @@ import mock from six.moves import StringIO import koji -from koji_cli.commands import anon_handle_list_channels +from koji_cli.commands.list_channels import anon_handle_list_channels from . import utils @@ -18,7 +18,7 @@ class TestListChannels(utils.CliTestCase): self.options.quiet = True self.session = mock.MagicMock() self.session.getAPIVersion.return_value = koji.API_VERSION - self.ensure_connection_mock = mock.patch('koji_cli.commands.ensure_connection').start() + self.ensure_connection_mock = mock.patch('koji_cli.commands.list_channels.ensure_connection').start() self.list_channels = [ {'id': 1, 'name': 'default', 'enabled': True, 'comment': 'test-comment-1', 'description': 'test-description-1'}, diff --git a/tests/test_cli/test_list_groups.py b/tests/test_cli/test_list_groups.py index cb7501a..c1b7215 100644 --- a/tests/test_cli/test_list_groups.py +++ b/tests/test_cli/test_list_groups.py @@ -14,7 +14,7 @@ class TestListGroups(utils.CliTestCase): self.session = mock.MagicMock() self.options = mock.MagicMock() - self.ensure_connection_mock = mock.patch('koji_cli.commands.ensure_connection').start() + self.ensure_connection_mock = mock.patch('koji_cli.lib.ensure_connection').start() self.event_from_opts = mock.patch('koji.util.eventFromOpts').start() self.error_format = """Usage: %s list-groups [options] [] @@ -56,7 +56,7 @@ class TestListGroups(utils.CliTestCase): expected = "Querying at event %(id)i (%(timestr)s)" % event + "\n" self.__list_groups('build', ['--ts', '1234567'], expected) - @mock.patch('koji_cli.commands.ensure_connection') + @mock.patch('koji_cli.lib.ensure_connection') def __list_groups(self, query_group, options, expected, ensure_connection_mock): _list_tags = [ { diff --git a/tests/test_cli/test_list_history.py b/tests/test_cli/test_list_history.py index 656b5a4..e9220f0 100644 --- a/tests/test_cli/test_list_history.py +++ b/tests/test_cli/test_list_history.py @@ -10,7 +10,7 @@ from six.moves import StringIO import mock import koji -from koji_cli.commands import anon_handle_list_history +from koji_cli.commands.list_history import anon_handle_list_history from . import utils @@ -20,7 +20,7 @@ class TestListHistory(utils.CliTestCase): self.options.debug = False self.session = mock.MagicMock() self.maxDiff = None - self.ensure_connection = mock.patch('koji_cli.commands.ensure_connection').start() + self.ensure_connection = mock.patch('koji_cli.commands.list_history.ensure_connection').start() self.error_format = """Usage: %s list-history [options] (Specify the --help global option for a list of other help options) diff --git a/tests/test_cli/test_list_hosts.py b/tests/test_cli/test_list_hosts.py index f9d1955..512163d 100644 --- a/tests/test_cli/test_list_hosts.py +++ b/tests/test_cli/test_list_hosts.py @@ -6,7 +6,7 @@ import locale from six.moves import StringIO import koji -from koji_cli.commands import anon_handle_list_hosts +from koji_cli.commands.list_hosts import anon_handle_list_hosts from . import utils @@ -22,7 +22,7 @@ class TestListHosts(utils.CliTestCase): self.original_timezone = os.environ.get('TZ') os.environ['TZ'] = 'UTC' time.tzset() - self.ensure_connection_mock = mock.patch('koji_cli.commands.ensure_connection').start() + self.ensure_connection_mock = mock.patch('koji_cli.commands.list_hosts.ensure_connection').start() self.error_format = """Usage: %s list-hosts [options] (Specify the --help global option for a list of other help options) diff --git a/tests/test_cli/test_list_notifications.py b/tests/test_cli/test_list_notifications.py index b69f68f..9a6f189 100644 --- a/tests/test_cli/test_list_notifications.py +++ b/tests/test_cli/test_list_notifications.py @@ -4,7 +4,7 @@ import mock from six.moves import StringIO import koji -from koji_cli.commands import anon_handle_list_notifications +from koji_cli.commands.list_notifications import anon_handle_list_notifications from . import utils @@ -15,8 +15,8 @@ class TestListNotifications(utils.CliTestCase): self.options.debug = False self.session = mock.MagicMock() self.session.getAPIVersion.return_value = koji.API_VERSION - self.ensure_connection_mock = mock.patch('koji_cli.commands.ensure_connection').start() - self.activate_session_mock = mock.patch('koji_cli.commands.activate_session').start() + self.ensure_connection_mock = mock.patch('koji_cli.commands.list_notifications.ensure_connection').start() + self.activate_session_mock = mock.patch('koji_cli.commands.list_notifications.activate_session').start() self.error_format = """Usage: %s list-notifications [options] (Specify the --help global option for a list of other help options) diff --git a/tests/test_cli/test_list_permissions.py b/tests/test_cli/test_list_permissions.py index 4f0afd1..69a5766 100644 --- a/tests/test_cli/test_list_permissions.py +++ b/tests/test_cli/test_list_permissions.py @@ -3,7 +3,7 @@ import mock import six import unittest -from koji_cli.commands import handle_list_permissions +from koji_cli.commands.list_permissions import handle_list_permissions from . import utils @@ -17,7 +17,7 @@ class TestListPermissions(utils.CliTestCase): """ % (self.progname, self.progname) self.session = mock.MagicMock() - self.activate_session_mock = mock.patch('koji_cli.commands.activate_session').start() + self.activate_session_mock = mock.patch('koji_cli.commands.list_permissions.activate_session').start() self.options = mock.MagicMock() self.options.quiet = True # Show long diffs in error output... diff --git a/tests/test_cli/test_list_pkgs.py b/tests/test_cli/test_list_pkgs.py index adfa7ba..080c701 100644 --- a/tests/test_cli/test_list_pkgs.py +++ b/tests/test_cli/test_list_pkgs.py @@ -5,7 +5,7 @@ import copy from six.moves import StringIO import koji -from koji_cli.commands import anon_handle_list_pkgs +from koji_cli.commands.list_pkgs import anon_handle_list_pkgs from . import utils @@ -15,7 +15,8 @@ class TestListPkgs(utils.CliTestCase): self.options.debug = False self.session = mock.MagicMock() self.session.getAPIVersion.return_value = koji.API_VERSION - self.ensure_connection_mock = mock.patch('koji_cli.commands.ensure_connection').start() + self.ensure_connection_mock = \ + mock.patch('koji_cli.commands.list_pkgs.ensure_connection').start() self.error_format = """Usage: %s list-pkgs [options] (Specify the --help global option for a list of other help options) diff --git a/tests/test_cli/test_list_tagged.py b/tests/test_cli/test_list_tagged.py index 867a178..47f6b2f 100644 --- a/tests/test_cli/test_list_tagged.py +++ b/tests/test_cli/test_list_tagged.py @@ -4,7 +4,7 @@ import time import mock import six -from koji_cli.commands import anon_handle_list_tagged +from koji_cli.commands.list_tagged import anon_handle_list_tagged from . import utils @@ -59,7 +59,7 @@ class TestCliListTagged(utils.CliTestCase): 'tag_name': 'tag', 'owner_name': 'owner', 'extra': 'extra-value-2'}] - self.ensure_connection_mock = mock.patch('koji_cli.commands.ensure_connection').start() + self.ensure_connection_mock = mock.patch('koji_cli.commands.list_tagged.ensure_connection').start() def tearDown(self): if self.original_timezone is None: diff --git a/tests/test_cli/test_list_tags.py b/tests/test_cli/test_list_tags.py index 9b48f9e..67ebc4a 100644 --- a/tests/test_cli/test_list_tags.py +++ b/tests/test_cli/test_list_tags.py @@ -4,7 +4,7 @@ import mock from six.moves import StringIO import koji -from koji_cli.commands import anon_handle_list_tags +from koji_cli.commands.list_tags import anon_handle_list_tags from . import utils @@ -15,7 +15,7 @@ class TestListTags(utils.CliTestCase): 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.ensure_connection = mock.patch('koji_cli.commands.list_tags.ensure_connection').start() self.list_tags_api = [{'arches': '', 'id': 455, 'locked': False, diff --git a/tests/test_cli/test_list_targets.py b/tests/test_cli/test_list_targets.py index be8a0bc..a9eea8e 100644 --- a/tests/test_cli/test_list_targets.py +++ b/tests/test_cli/test_list_targets.py @@ -37,7 +37,7 @@ class TestCliListTargets(utils.CliTestCase): time.tzset() @mock.patch('sys.stderr', new_callable=six.StringIO) - @mock.patch('koji_cli.commands.ensure_connection') + @mock.patch('koji_cli.lib.ensure_connection') def test_list_targets_error_args(self, ensure_connection_mock, stderr): session = mock.MagicMock(getAPIVersion=lambda: koji.API_VERSION, getBuildTargets=lambda n: []) @@ -47,7 +47,7 @@ class TestCliListTargets(utils.CliTestCase): self.assertExitCode(ex, 2) @mock.patch('sys.stderr', new_callable=six.StringIO) - @mock.patch('koji_cli.commands.ensure_connection') + @mock.patch('koji_cli.lib.ensure_connection') def test_list_targets_error_all_not_found(self, ensure_connection_mock, stderr): session = mock.MagicMock(getAPIVersion=lambda: koji.API_VERSION, getBuildTargets=lambda n: []) @@ -60,7 +60,7 @@ class TestCliListTargets(utils.CliTestCase): @mock.patch('optparse.OptionParser.parse_args', return_value=(Values({'quiet': False, 'name': 'f50'}), [])) @mock.patch('sys.stderr', new_callable=six.StringIO) - @mock.patch('koji_cli.commands.ensure_connection') + @mock.patch('koji_cli.lib.ensure_connection') def test_list_targets_error_name_not_found(self, ensure_connection_mock, stderr, opt): session = mock.MagicMock(getAPIVersion=lambda: koji.API_VERSION, getBuildTargets=lambda n: []) @@ -71,7 +71,7 @@ class TestCliListTargets(utils.CliTestCase): self.assertTrue('No such build target:' in stderr.getvalue()) @mock.patch('sys.stdout', new_callable=six.StringIO) - @mock.patch('koji_cli.commands.ensure_connection') + @mock.patch('koji_cli.lib.ensure_connection') def test_list_targets_all(self, ensure_connection_mock, stdout): session = mock.MagicMock(getAPIVersion=lambda: koji.API_VERSION, getBuildTargets=lambda n: _mock_targets) @@ -88,7 +88,7 @@ class TestCliListTargets(utils.CliTestCase): @mock.patch('optparse.OptionParser.parse_args', return_value=(Values({'quiet': False, 'name': 'f50'}), [])) @mock.patch('sys.stdout', new_callable=six.StringIO) - @mock.patch('koji_cli.commands.ensure_connection') + @mock.patch('koji_cli.lib.ensure_connection') def test_list_targets_one(self, ensure_connection_mock, stdout, opt): session = mock.MagicMock(getAPIVersion=lambda: koji.API_VERSION, getBuildTargets=lambda n: _mock_targets) diff --git a/tests/test_cli/test_list_tasks.py b/tests/test_cli/test_list_tasks.py index ddf67dd..cc2b6fc 100644 --- a/tests/test_cli/test_list_tasks.py +++ b/tests/test_cli/test_list_tasks.py @@ -7,7 +7,7 @@ import six import koji from koji_cli.lib import _list_tasks -from koji_cli.commands import handle_list_tasks +from koji_cli.commands.list_tasks import handle_list_tasks from . import utils @@ -172,8 +172,8 @@ class TestCliListTasks(utils.CliTestCase): """ % (self.progname, self.progname) @mock.patch('sys.stdout', new_callable=six.StringIO) - @mock.patch('koji_cli.commands._list_tasks') - @mock.patch('koji_cli.commands.activate_session') + @mock.patch('koji_cli.commands.list_tasks._list_tasks') + @mock.patch('koji_cli.commands.list_tasks.activate_session') def test_handle_list_tasks( self, activate_session_mock, diff --git a/tests/test_cli/test_list_untagged.py b/tests/test_cli/test_list_untagged.py index fffa7a3..fd426ee 100644 --- a/tests/test_cli/test_list_untagged.py +++ b/tests/test_cli/test_list_untagged.py @@ -22,7 +22,7 @@ class TestListUntagged(utils.CliTestCase): ] @mock.patch('sys.stdout', new_callable=StringIO) - @mock.patch('koji_cli.commands.ensure_connection') + @mock.patch('koji_cli.lib.ensure_connection') def test_list_untagged_without_arguments(self, ensure_connection_mock, stdout): package_name = 'test-package-1234' @@ -33,7 +33,7 @@ class TestListUntagged(utils.CliTestCase): anon_handle_list_untagged(self.options, self.session, [package_name]) self.assert_console_message(stdout, expected) - @mock.patch('koji_cli.commands.ensure_connection') + @mock.patch('koji_cli.lib.ensure_connection') def test_list_untagged_more_arguments(self, ensure_connection_mock): packages_name = ['test-package-1', 'test-package-2'] expected = """Usage: %s list-untagged [options] [] @@ -51,7 +51,7 @@ class TestListUntagged(utils.CliTestCase): @mock.patch('sys.stderr', new_callable=StringIO) @mock.patch('sys.stdout', new_callable=StringIO) - @mock.patch('koji_cli.commands.ensure_connection') + @mock.patch('koji_cli.lib.ensure_connection') def test_list_untagged_package(self, ensure_connection, stdout, stderr): # test case when package is existing package_name = 'test-package-1234' @@ -76,7 +76,7 @@ class TestListUntagged(utils.CliTestCase): self.assert_console_message(stderr, expected) @mock.patch('sys.stdout', new_callable=StringIO) - @mock.patch('koji_cli.commands.ensure_connection') + @mock.patch('koji_cli.lib.ensure_connection') def test_list_untagged_package_path(self, ensure_connection, stdout): # test case when package is existing package_name = 'test-package-1234' diff --git a/tests/test_cli/test_list_volumes.py b/tests/test_cli/test_list_volumes.py index 890eba2..84f8384 100644 --- a/tests/test_cli/test_list_volumes.py +++ b/tests/test_cli/test_list_volumes.py @@ -13,7 +13,7 @@ class TestListVolumes(utils.CliTestCase): maxDiff = None @mock.patch('sys.stdout', new_callable=six.StringIO) - @mock.patch('koji_cli.commands.ensure_connection') + @mock.patch('koji_cli.lib.ensure_connection') def test_anon_handle_list_volumes( self, ensure_connection_mock, diff --git a/tests/test_cli/test_maven_build.py b/tests/test_cli/test_maven_build.py index 840b35f..3498f86 100644 --- a/tests/test_cli/test_maven_build.py +++ b/tests/test_cli/test_maven_build.py @@ -7,7 +7,7 @@ import six import sys import unittest -from koji_cli.commands import handle_maven_build +from koji_cli.commands.maven_build import handle_maven_build from . import utils EMPTY_BUILD_OPTS = { @@ -44,9 +44,9 @@ class TestMavenBuild(utils.CliTestCase): self.session = mock.MagicMock() @mock.patch('sys.stdout', new_callable=six.StringIO) - @mock.patch('koji_cli.commands.activate_session') - @mock.patch('koji_cli.commands._running_in_bg', return_value=False) - @mock.patch('koji_cli.commands.watch_tasks', return_value=0) + @mock.patch('koji_cli.commands.maven_build.activate_session') + @mock.patch('koji_cli.commands.maven_build._running_in_bg', return_value=False) + @mock.patch('koji_cli.commands.maven_build.watch_tasks', return_value=0) def test_handle_maven_build(self, watch_tasks_mock, running_in_bg_mock, activate_session_mock, stdout): target = 'target' @@ -87,9 +87,9 @@ Task info: weburl/taskinfo?taskID=1 @mock.patch('sys.stdout', new_callable=six.StringIO) @mock.patch('sys.stderr', new_callable=six.StringIO) - @mock.patch('koji_cli.commands.activate_session') - @mock.patch('koji_cli.commands._running_in_bg', return_value=False) - @mock.patch('koji_cli.commands.watch_tasks', return_value=0) + @mock.patch('koji_cli.commands.maven_build.activate_session') + @mock.patch('koji_cli.commands.maven_build._running_in_bg', return_value=False) + @mock.patch('koji_cli.commands.maven_build.watch_tasks', return_value=0) def test_handle_maven_build_no_arg( self, watch_tasks_mock, @@ -127,9 +127,9 @@ Task info: weburl/taskinfo?taskID=1 @mock.patch('sys.stdout', new_callable=six.StringIO) @mock.patch('sys.stderr', new_callable=six.StringIO) - @mock.patch('koji_cli.commands.activate_session') - @mock.patch('koji_cli.commands._running_in_bg', return_value=False) - @mock.patch('koji_cli.commands.watch_tasks', return_value=0) + @mock.patch('koji_cli.commands.maven_build.activate_session') + @mock.patch('koji_cli.commands.maven_build._running_in_bg', return_value=False) + @mock.patch('koji_cli.commands.maven_build.watch_tasks', return_value=0) def test_handle_maven_build_no_arg_with_ini( self, watch_tasks_mock, @@ -167,9 +167,9 @@ Task info: weburl/taskinfo?taskID=1 @mock.patch('sys.stdout', new_callable=six.StringIO) @mock.patch('sys.stderr', new_callable=six.StringIO) - @mock.patch('koji_cli.commands.activate_session') - @mock.patch('koji_cli.commands._running_in_bg', return_value=False) - @mock.patch('koji_cli.commands.watch_tasks', return_value=0) + @mock.patch('koji_cli.commands.maven_build.activate_session') + @mock.patch('koji_cli.commands.maven_build._running_in_bg', return_value=False) + @mock.patch('koji_cli.commands.maven_build.watch_tasks', return_value=0) def test_handle_maven_build_help( self, watch_tasks_mock, @@ -235,9 +235,9 @@ Options: watch_tasks_mock.assert_not_called() @mock.patch('sys.stderr', new_callable=six.StringIO) - @mock.patch('koji_cli.commands.activate_session') - @mock.patch('koji_cli.commands._running_in_bg', return_value=False) - @mock.patch('koji_cli.commands.watch_tasks', return_value=0) + @mock.patch('koji_cli.commands.maven_build.activate_session') + @mock.patch('koji_cli.commands.maven_build._running_in_bg', return_value=False) + @mock.patch('koji_cli.commands.maven_build.watch_tasks', return_value=0) def test_handle_maven_build_target_not_found( self, watch_tasks_mock, @@ -276,9 +276,9 @@ Options: watch_tasks_mock.assert_not_called() @mock.patch('sys.stderr', new_callable=six.StringIO) - @mock.patch('koji_cli.commands.activate_session') - @mock.patch('koji_cli.commands._running_in_bg', return_value=False) - @mock.patch('koji_cli.commands.watch_tasks', return_value=0) + @mock.patch('koji_cli.commands.maven_build.activate_session') + @mock.patch('koji_cli.commands.maven_build._running_in_bg', return_value=False) + @mock.patch('koji_cli.commands.maven_build.watch_tasks', return_value=0) def test_handle_build_dest_tag_not_found( self, watch_tasks_mock, @@ -321,9 +321,9 @@ Options: watch_tasks_mock.assert_not_called() @mock.patch('sys.stderr', new_callable=six.StringIO) - @mock.patch('koji_cli.commands.activate_session') - @mock.patch('koji_cli.commands._running_in_bg', return_value=False) - @mock.patch('koji_cli.commands.watch_tasks', return_value=0) + @mock.patch('koji_cli.commands.maven_build.activate_session') + @mock.patch('koji_cli.commands.maven_build._running_in_bg', return_value=False) + @mock.patch('koji_cli.commands.maven_build.watch_tasks', return_value=0) def test_handle_build_dest_tag_locked( self, watch_tasks_mock, @@ -367,7 +367,7 @@ Options: @mock.patch('sys.stderr', new_callable=six.StringIO) @mock.patch('sys.stdout', new_callable=six.StringIO) - @mock.patch('koji_cli.commands.activate_session') + @mock.patch('koji_cli.commands.maven_build.activate_session') @mock.patch( 'koji.util.parse_maven_param', return_value={ @@ -377,8 +377,8 @@ Options: 'pkg1', 'pkg2']}}) @mock.patch('koji.util.maven_opts') - @mock.patch('koji_cli.commands._running_in_bg', return_value=False) - @mock.patch('koji_cli.commands.watch_tasks', return_value=0) + @mock.patch('koji_cli.commands.maven_build._running_in_bg', return_value=False) + @mock.patch('koji_cli.commands.maven_build.watch_tasks', return_value=0) def test_handle_build_inis( self, watch_tasks_mock, @@ -500,11 +500,11 @@ Task info: weburl/taskinfo?taskID=1 self.session.mavenBuild.assert_not_called() @mock.patch('sys.stderr', new_callable=six.StringIO) - @mock.patch('koji_cli.commands.activate_session') + @mock.patch('koji_cli.commands.maven_build.activate_session') @mock.patch('koji.util.parse_maven_param') @mock.patch('koji.util.maven_opts') - @mock.patch('koji_cli.commands._running_in_bg', return_value=False) - @mock.patch('koji_cli.commands.watch_tasks', return_value=0) + @mock.patch('koji_cli.commands.maven_build._running_in_bg', return_value=False) + @mock.patch('koji_cli.commands.maven_build.watch_tasks', return_value=0) def test_handle_build_invalid_scm( self, watch_tasks_mock, @@ -552,11 +552,11 @@ Task info: weburl/taskinfo?taskID=1 watch_tasks_mock.assert_not_called() @mock.patch('sys.stdout', new_callable=six.StringIO) - @mock.patch('koji_cli.commands.activate_session') + @mock.patch('koji_cli.commands.maven_build.activate_session') @mock.patch('koji.util.parse_maven_param') @mock.patch('koji.util.maven_opts', return_value={}) - @mock.patch('koji_cli.commands._running_in_bg', return_value=False) - @mock.patch('koji_cli.commands.watch_tasks', return_value=0) + @mock.patch('koji_cli.commands.maven_build._running_in_bg', return_value=False) + @mock.patch('koji_cli.commands.maven_build.watch_tasks', return_value=0) def test_handle_build_other_params( self, watch_tasks_mock, @@ -635,9 +635,9 @@ Task info: weburl/taskinfo?taskID=1 source, target, opts, priority=priority) @mock.patch('sys.stdout', new_callable=six.StringIO) - @mock.patch('koji_cli.commands.activate_session') - @mock.patch('koji_cli.commands._running_in_bg', return_value=False) - @mock.patch('koji_cli.commands.watch_tasks', return_value=0) + @mock.patch('koji_cli.commands.maven_build.activate_session') + @mock.patch('koji_cli.commands.maven_build._running_in_bg', return_value=False) + @mock.patch('koji_cli.commands.maven_build.watch_tasks', return_value=0) def test_handle_maven_build_quiet( self, watch_tasks_mock, @@ -680,9 +680,9 @@ Task info: weburl/taskinfo?taskID=1 self.assertEqual(rv, 0) @mock.patch('sys.stdout', new_callable=six.StringIO) - @mock.patch('koji_cli.commands.activate_session') - @mock.patch('koji_cli.commands._running_in_bg', return_value=True) - @mock.patch('koji_cli.commands.watch_tasks', return_value=0) + @mock.patch('koji_cli.commands.maven_build.activate_session') + @mock.patch('koji_cli.commands.maven_build._running_in_bg', return_value=True) + @mock.patch('koji_cli.commands.maven_build.watch_tasks', return_value=0) def test_handle_maven_build_quiet( self, watch_tasks_mock, @@ -724,10 +724,10 @@ Task info: weburl/taskinfo?taskID=1 self.assertIsNone(rv) @mock.patch('sys.stdout', new_callable=six.StringIO) - @mock.patch('koji_cli.commands.activate_session') + @mock.patch('koji_cli.commands.maven_build.activate_session') @mock.patch('koji.util.parse_maven_param') @mock.patch('koji.util.maven_opts', return_value={}) - @mock.patch('koji_cli.commands.watch_tasks', return_value=0) + @mock.patch('koji_cli.commands.maven_build.watch_tasks', return_value=0) def test_handle_maven_build_nowait( self, watch_tasks_mock, diff --git a/tests/test_cli/test_maven_chain.py b/tests/test_cli/test_maven_chain.py index b87078f..b1a8858 100644 --- a/tests/test_cli/test_maven_chain.py +++ b/tests/test_cli/test_maven_chain.py @@ -1,9 +1,10 @@ from __future__ import absolute_import +from koji_cli.lib import activate_session import mock import six import unittest -from koji_cli.commands import handle_maven_chain +from koji_cli.commands.maven_chain import handle_maven_chain from . import utils @@ -26,9 +27,9 @@ class TestMavenChain(utils.CliTestCase): @mock.patch('sys.stdout', new_callable=six.StringIO) @mock.patch('sys.stderr', new_callable=six.StringIO) @mock.patch('koji.util.parse_maven_chain') - @mock.patch('koji_cli.commands._running_in_bg', return_value=False) - @mock.patch('koji_cli.commands.watch_tasks') - @mock.patch('koji_cli.commands.activate_session') + @mock.patch('koji_cli.commands.maven_chain._running_in_bg', return_value=False) + @mock.patch('koji_cli.commands.maven_chain.watch_tasks') + @mock.patch('koji_cli.commands.maven_chain.activate_session') def test_handle_maven_chain( self, activate_session_mock, @@ -66,7 +67,8 @@ class TestMavenChain(utils.CliTestCase): options, session, arguments, - stderr=expected) + stderr=expected, + activate_session=None) # Unknow destination tag test session.getBuildTarget.return_value = target_info @@ -77,7 +79,8 @@ class TestMavenChain(utils.CliTestCase): options, session, arguments, - stderr=expected) + stderr=expected, + activate_session=None) # Distination is locked and --scratch is not specified session.getTag.return_value = tag_info @@ -88,7 +91,8 @@ class TestMavenChain(utils.CliTestCase): options, session, arguments, - stderr=expected) + stderr=expected, + activate_session=None) # Test ValueError exception asserted in parse_maven_chain arguments.extend(['--skip-tag', '--scratch', @@ -100,7 +104,8 @@ class TestMavenChain(utils.CliTestCase): options, session, arguments, - stderr=expected) + stderr=expected, + activate_session=None) # Background or --nowait is true parse_maven_chain_mock.side_effect = None @@ -140,7 +145,7 @@ class TestMavenChain(utils.CliTestCase): @mock.patch('sys.stdout', new_callable=six.StringIO) @mock.patch('sys.stderr', new_callable=six.StringIO) - @mock.patch('koji_cli.commands.activate_session') + @mock.patch('koji_cli.commands.maven_chain.activate_session') def test_handle_maven_no_argument_error( self, activate_session_mock, diff --git a/tests/test_cli/test_mock_config.py b/tests/test_cli/test_mock_config.py index bf7d968..ce7d2b8 100644 --- a/tests/test_cli/test_mock_config.py +++ b/tests/test_cli/test_mock_config.py @@ -6,7 +6,7 @@ import mock import six import koji -from koji_cli.commands import anon_handle_mock_config +from koji_cli.commands.mock_config import anon_handle_mock_config from . import utils @@ -21,7 +21,7 @@ class TestMockConfig(utils.CliTestCase): self.session = mock.MagicMock() self.session.getAPIVersion.return_value = koji.API_VERSION self.gen_config_mock = mock.patch('koji.genMockConfig').start() - self.ensure_connection_mock = mock.patch('koji_cli.commands.ensure_connection').start() + self.ensure_connection_mock = mock.patch('koji_cli.commands.mock_config.ensure_connection').start() self.maxDiff = None self.common_args = [ '--distribution', 'fedora', @@ -321,7 +321,7 @@ config_opts['macros']['%distribution'] = 'Koji Testing' @mock.patch('sys.stderr', new_callable=six.StringIO) @mock.patch('sys.stdout', new_callable=six.StringIO) - @mock.patch('koji_cli.commands.open') + @mock.patch('koji_cli.commands.mock_config.open') def test_handle_mock_config_target_option(self, openf, stdout, stderr): """Test anon_handle_mock_config with target option""" arch = "x86_64" diff --git a/tests/test_cli/test_move_build.py b/tests/test_cli/test_move_build.py index c836edb..68c5a2e 100644 --- a/tests/test_cli/test_move_build.py +++ b/tests/test_cli/test_move_build.py @@ -5,7 +5,7 @@ import unittest import mock import six -from koji_cli.commands import handle_move_build +from koji_cli.commands.move_build import handle_move_build from . import utils @@ -24,10 +24,10 @@ class TestMoveBuild(utils.CliTestCase): %s: error: {message} """ % (self.progname, self.progname) - self.activate_session = mock.patch('koji_cli.commands.activate_session').start() - self.running_in_bg = mock.patch('koji_cli.commands._running_in_bg').start() + self.activate_session = mock.patch('koji_cli.commands.move_build.activate_session').start() + self.running_in_bg = mock.patch('koji_cli.commands.move_build._running_in_bg').start() self.running_in_bg.return_value = False - self.watch_tasks = mock.patch('koji_cli.commands.watch_tasks').start() + self.watch_tasks = mock.patch('koji_cli.commands.move_build.watch_tasks').start() self.watch_tasks.return_value = True def tearDown(self): diff --git a/tests/test_cli/test_regen_repo.py b/tests/test_cli/test_regen_repo.py index e2beffc..dead734 100644 --- a/tests/test_cli/test_regen_repo.py +++ b/tests/test_cli/test_regen_repo.py @@ -3,11 +3,12 @@ from __future__ import print_function import unittest import copy +from koji_cli.lib import activate_session import mock import six -from koji_cli.commands import handle_regen_repo +from koji_cli.commands.regen_repo import handle_regen_repo from . import utils @@ -52,12 +53,12 @@ class TestRegenRepo(utils.CliTestCase): self.setUpMocks() def setUpMocks(self): - self.activate_session = mock.patch('koji_cli.commands.activate_session').start() + self.activate_session = mock.patch('koji_cli.commands.regen_repo.activate_session').start() - self.running_in_bg = mock.patch('koji_cli.commands._running_in_bg').start() + self.running_in_bg = mock.patch('koji_cli.commands.regen_repo._running_in_bg').start() self.running_in_bg.return_value = False # assume run in foreground - self.watch_tasks = mock.patch('koji_cli.commands.watch_tasks').start() + self.watch_tasks = mock.patch('koji_cli.commands.regen_repo.watch_tasks').start() self.watch_tasks.return_value = True self.mocks_table = {} @@ -96,7 +97,8 @@ class TestRegenRepo(utils.CliTestCase): self.options, self.session, [self.tag_name], - stderr=expected) + stderr=expected, + activate_session=None) self.resetMocks() @@ -128,7 +130,8 @@ class TestRegenRepo(utils.CliTestCase): self.options, self.session, arguments, - stderr=expected) + stderr=expected, + activate_session=None) self.resetMocks() diff --git a/tests/test_cli/test_remove_channel.py b/tests/test_cli/test_remove_channel.py index 785cdc2..32d8515 100644 --- a/tests/test_cli/test_remove_channel.py +++ b/tests/test_cli/test_remove_channel.py @@ -5,7 +5,7 @@ import unittest import six import mock -from koji_cli.commands import handle_remove_channel +from koji_cli.commands.remove_channel import handle_remove_channel from . import utils @@ -24,7 +24,7 @@ class TestRemoveChannel(utils.CliTestCase): self.maxDiff = None @mock.patch('sys.stdout', new_callable=six.StringIO) - @mock.patch('koji_cli.commands.activate_session') + @mock.patch('koji_cli.commands.remove_channel.activate_session') def test_handle_remove_channel(self, activate_session_mock, stdout): self.session.getChannel.return_value = self.channel_info rv = handle_remove_channel(self.options, self.session, [self.channel_name]) @@ -37,7 +37,7 @@ class TestRemoveChannel(utils.CliTestCase): self.assertNotEqual(rv, 1) @mock.patch('sys.stdout', new_callable=six.StringIO) - @mock.patch('koji_cli.commands.activate_session') + @mock.patch('koji_cli.commands.remove_channel.activate_session') def test_handle_remove_channel_force(self, activate_session_mock, stdout): self.session.getChannel.return_value = self.channel_info rv = handle_remove_channel(self.options, self.session, ['--force', self.channel_name]) @@ -50,7 +50,7 @@ class TestRemoveChannel(utils.CliTestCase): self.assertNotEqual(rv, 1) @mock.patch('sys.stderr', new_callable=six.StringIO) - @mock.patch('koji_cli.commands.activate_session') + @mock.patch('koji_cli.commands.remove_channel.activate_session') def test_handle_remove_channel_no_channel( self, activate_session_mock, stderr): channel_info = None @@ -68,7 +68,7 @@ class TestRemoveChannel(utils.CliTestCase): @mock.patch('sys.stdout', new_callable=six.StringIO) @mock.patch('sys.stderr', new_callable=six.StringIO) - @mock.patch('koji_cli.commands.activate_session') + @mock.patch('koji_cli.commands.remove_channel.activate_session') def test_handle_remove_channel_help( self, activate_session_mock, stderr, stdout): with self.assertRaises(SystemExit) as ex: diff --git a/tests/test_cli/test_remove_group.py b/tests/test_cli/test_remove_group.py index 2c0947e..c8125dd 100644 --- a/tests/test_cli/test_remove_group.py +++ b/tests/test_cli/test_remove_group.py @@ -3,7 +3,7 @@ from __future__ import absolute_import import mock import six -from koji_cli.commands import handle_remove_group +from koji_cli.commands.remove_group import handle_remove_group from . import utils @@ -23,7 +23,7 @@ class TestRemoveGroup(utils.CliTestCase): @mock.patch('sys.stdout', new_callable=six.StringIO) @mock.patch('sys.stderr', new_callable=six.StringIO) - @mock.patch('koji_cli.commands.activate_session') + @mock.patch('koji_cli.commands.remove_group.activate_session') def test_handle_remove_group_nonexistent_tag(self, activate_session_mock, stderr, stdout): tag = 'nonexistent-tag' group = 'group' @@ -48,7 +48,7 @@ class TestRemoveGroup(utils.CliTestCase): @mock.patch('sys.stdout', new_callable=six.StringIO) @mock.patch('sys.stderr', new_callable=six.StringIO) - @mock.patch('koji_cli.commands.activate_session') + @mock.patch('koji_cli.commands.remove_group.activate_session') def test_handle_remove_group_nonexistent_group(self, activate_session_mock, stdout, stderr): tag = 'tag' group = 'group' @@ -71,7 +71,7 @@ class TestRemoveGroup(utils.CliTestCase): @mock.patch('sys.stdout', new_callable=six.StringIO) @mock.patch('sys.stderr', new_callable=six.StringIO) - @mock.patch('koji_cli.commands.activate_session') + @mock.patch('koji_cli.commands.remove_group.activate_session') def test_handle_remove_group(self, activate_session_mock, stdout, stderr): tag = 'tag' group = 'group' @@ -95,7 +95,7 @@ class TestRemoveGroup(utils.CliTestCase): @mock.patch('sys.stdout', new_callable=six.StringIO) @mock.patch('sys.stderr', new_callable=six.StringIO) - @mock.patch('koji_cli.commands.activate_session') + @mock.patch('koji_cli.commands.remove_group.activate_session') def test_handle_remove_group_error_handling(self, activate_session_mock, stdout, stderr): expected = self.format_error_message( "Please specify a tag name and a group name") diff --git a/tests/test_cli/test_remove_host_from_channel.py b/tests/test_cli/test_remove_host_from_channel.py index 05ba32b..5f0de64 100644 --- a/tests/test_cli/test_remove_host_from_channel.py +++ b/tests/test_cli/test_remove_host_from_channel.py @@ -5,16 +5,17 @@ import six import sys import unittest -from koji_cli.commands import handle_remove_host_from_channel +from koji_cli.commands.remove_host_from_channel import handle_remove_host_from_channel from . import utils + class TestRemoveHostFromChannel(utils.CliTestCase): # Show long diffs in error output... maxDiff = None @mock.patch('sys.stdout', new_callable=six.StringIO) - @mock.patch('koji_cli.commands.activate_session') + @mock.patch('koji_cli.commands.remove_host_from_channel.activate_session') def test_handle_remove_host_from_channel( self, activate_session_mock, stdout): host = 'host' @@ -44,7 +45,7 @@ class TestRemoveHostFromChannel(utils.CliTestCase): self.assertNotEqual(rv, 1) @mock.patch('sys.stderr', new_callable=six.StringIO) - @mock.patch('koji_cli.commands.activate_session') + @mock.patch('koji_cli.commands.remove_host_from_channel.activate_session') def test_handle_remove_host_from_channel_no_host( self, activate_session_mock, stderr): host = 'host' @@ -73,7 +74,7 @@ class TestRemoveHostFromChannel(utils.CliTestCase): session.removeHostFromChannel.assert_not_called() @mock.patch('sys.stderr', new_callable=six.StringIO) - @mock.patch('koji_cli.commands.activate_session') + @mock.patch('koji_cli.commands.remove_host_from_channel.activate_session') def test_handle_remove_host_from_channel_not_a_member( self, activate_session_mock, stderr): host = 'host' @@ -106,7 +107,7 @@ class TestRemoveHostFromChannel(utils.CliTestCase): @mock.patch('sys.stdout', new_callable=six.StringIO) @mock.patch('sys.stderr', new_callable=six.StringIO) - @mock.patch('koji_cli.commands.activate_session') + @mock.patch('koji_cli.commands.remove_host_from_channel.activate_session') def test_handle_remove_host_from_channel_help( self, activate_session_mock, stderr, stdout): args = [] diff --git a/tests/test_cli/test_remove_notification.py b/tests/test_cli/test_remove_notification.py index b07fbd3..a09eded 100644 --- a/tests/test_cli/test_remove_notification.py +++ b/tests/test_cli/test_remove_notification.py @@ -4,7 +4,7 @@ import mock from six.moves import StringIO import koji -from koji_cli.commands import handle_remove_notification +from koji_cli.commands.remove_notification import handle_remove_notification from . import utils @@ -15,14 +15,14 @@ class TestAddHost(utils.CliTestCase): self.session = mock.MagicMock() self.session.getAPIVersion.return_value = koji.API_VERSION - @mock.patch('koji_cli.commands.activate_session') + @mock.patch('koji_cli.commands.remove_notification.activate_session') def test_handle_remove_notification(self, activate_session_mock): handle_remove_notification(self.options, self.session, ['1', '3', '5']) self.session.deleteNotification.assert_has_calls([mock.call(1), mock.call(3), mock.call(5)]) - @mock.patch('koji_cli.commands.activate_session') + @mock.patch('koji_cli.commands.remove_notification.activate_session') @mock.patch('sys.stdout', new_callable=StringIO) def test_handle_remove_notification_not_quiet(self, stdout, activate_session_mock): self.options.quiet = False diff --git a/tests/test_cli/test_remove_pkg.py b/tests/test_cli/test_remove_pkg.py index 5a0f404..94a262b 100644 --- a/tests/test_cli/test_remove_pkg.py +++ b/tests/test_cli/test_remove_pkg.py @@ -6,7 +6,7 @@ import mock import six from mock import call -from koji_cli.commands import handle_remove_pkg +from koji_cli.commands.remove_pkg import handle_remove_pkg import koji from . import utils @@ -21,7 +21,7 @@ class TestRemovePkg(utils.CliTestCase): self.options.debug = False self.session = mock.MagicMock() self.session.getAPIVersion.return_value = koji.API_VERSION - self.activate_session_mock = mock.patch('koji_cli.commands.activate_session').start() + self.activate_session_mock = mock.patch('koji_cli.commands.remove_pkg.activate_session').start() self.error_format = """Usage: %s remove-pkg [options] [ ...] (Specify the --help global option for a list of other help options) diff --git a/tests/test_cli/test_remove_tag_inheritance.py b/tests/test_cli/test_remove_tag_inheritance.py index f97a3ae..183ffd2 100644 --- a/tests/test_cli/test_remove_tag_inheritance.py +++ b/tests/test_cli/test_remove_tag_inheritance.py @@ -4,7 +4,7 @@ import mock from mock import call import koji -from koji_cli.commands import handle_remove_tag_inheritance +from koji_cli.commands.remove_tag_inheritance import handle_remove_tag_inheritance from . import utils @@ -14,7 +14,7 @@ class TestRemoveTagInheritance(utils.CliTestCase): self.options.debug = False self.session = mock.MagicMock() self.session.getAPIVersion.return_value = koji.API_VERSION - self.activate_session_mock = mock.patch('koji_cli.commands.activate_session').start() + self.activate_session_mock = mock.patch('koji_cli.commands.remove_tag_inheritance.activate_session').start() self.error_format = """Usage: %s remove-tag-inheritance (Specify the --help global option for a list of other help options) diff --git a/tests/test_cli/test_restart_host.py b/tests/test_cli/test_restart_host.py index 7801148..111be91 100644 --- a/tests/test_cli/test_restart_host.py +++ b/tests/test_cli/test_restart_host.py @@ -4,7 +4,7 @@ import six import unittest import koji -from koji_cli.commands import handle_restart_hosts +from koji_cli.commands.restart_hosts import handle_restart_hosts from . import utils @@ -18,10 +18,10 @@ class TestRestartHosts(utils.CliTestCase): self.options.quiet = None self.options.poll_interval = 3 self.session = mock.MagicMock() - self.activate_session_mock = mock.patch('koji_cli.commands.activate_session').start() - self.running_in_bg_mock = mock.patch('koji_cli.commands._running_in_bg').start() + self.activate_session_mock = mock.patch('koji_cli.commands.restart_hosts.activate_session').start() + self.running_in_bg_mock = mock.patch('koji_cli.commands.restart_hosts._running_in_bg').start() self.running_in_bg_mock.return_value = False - self.watch_tasks_mock = mock.patch('koji_cli.commands.watch_tasks').start() + self.watch_tasks_mock = mock.patch('koji_cli.commands.restart_hosts.watch_tasks').start() self.task_id = 101 def test_handle_restart_hosts_force_options(self): @@ -104,7 +104,7 @@ class TestRestartHosts(utils.CliTestCase): expect += "Use --force to run anyway\n" self.assert_console_message(stderr, expect) - @mock.patch('koji_cli.commands._running_in_bg') + @mock.patch('koji_cli.commands.restart_hosts._running_in_bg') def test_handle_restart_hosts_wait_option(self, running_in_bg_mock): """Test %s function with --force option""" % handle_restart_hosts.__name__ arguments = ['--wait'] @@ -128,7 +128,7 @@ class TestRestartHosts(utils.CliTestCase): self.watch_tasks_mock.assert_called_with( self.session, [self.task_id], quiet=None, poll_interval=3, topurl=self.options.topurl) - @mock.patch('koji_cli.commands._running_in_bg') + @mock.patch('koji_cli.commands.restart_hosts._running_in_bg') def test_handle_restart_hosts_other_options(self, running_in_bg_mock): """Test %s function with --force option""" % handle_restart_hosts.__name__ arguments = ['--nowait', @@ -156,7 +156,7 @@ class TestRestartHosts(utils.CliTestCase): self.watch_tasks_mock.assert_not_called() @mock.patch('sys.stderr', new_callable=six.StringIO) - @mock.patch('koji_cli.commands._running_in_bg') + @mock.patch('koji_cli.commands.restart_hosts._running_in_bg') def test_handle_restart_hosts_arguments(self, running_in_bg_mock, stderr): """Test %s function with --force option""" % handle_restart_hosts.__name__ diff --git a/tests/test_cli/test_resubmit.py b/tests/test_cli/test_resubmit.py index 75a6222..745b59b 100644 --- a/tests/test_cli/test_resubmit.py +++ b/tests/test_cli/test_resubmit.py @@ -4,7 +4,7 @@ import mock import six import unittest -from koji_cli.commands import handle_resubmit +from koji_cli.commands.resubmit import handle_resubmit from . import utils @@ -36,8 +36,8 @@ Log Files: @mock.patch('sys.stdout', new_callable=six.StringIO) @mock.patch('sys.stderr', new_callable=six.StringIO) - @mock.patch('koji_cli.commands.watch_tasks') - @mock.patch('koji_cli.commands.activate_session') + @mock.patch('koji_cli.commands.resubmit.watch_tasks') + @mock.patch('koji_cli.commands.resubmit.activate_session') def test_handle_resubmit( self, activate_session_mock, @@ -56,7 +56,7 @@ Log Files: session.getTaskInfo.return_value = None # Generate task info and nowait tests - with mock.patch('koji_cli.commands._printTaskInfo') as p_mock: + with mock.patch('koji_cli.commands.resubmit._printTaskInfo') as p_mock: p_mock.side_effect = lambda *args, **kwargs: print(self.taskinfo) handle_resubmit(options, session, arguments + ['--nowait']) activate_session_mock.assert_called_with(session, options) @@ -72,7 +72,7 @@ Log Files: # Quiet and watch tasks tests arguments.append('--quiet') - with mock.patch('koji_cli.commands._running_in_bg') as run_bg_mock: + with mock.patch('koji_cli.commands.resubmit._running_in_bg') as run_bg_mock: run_bg_mock.return_value = False self.assertTrue(handle_resubmit(options, session, arguments)) run_bg_mock.assert_called_once() @@ -89,7 +89,7 @@ Log Files: @mock.patch('sys.stdout', new_callable=six.StringIO) @mock.patch('sys.stderr', new_callable=six.StringIO) - @mock.patch('koji_cli.commands.activate_session') + @mock.patch('koji_cli.commands.resubmit.activate_session') def test_handle_resubmit_argument_error( self, activate_session_mock, stderr, stdout): """Test handle_resubmit argument error""" diff --git a/tests/test_cli/test_revoke_cg_access.py b/tests/test_cli/test_revoke_cg_access.py index 8b3107c..fac930f 100644 --- a/tests/test_cli/test_revoke_cg_access.py +++ b/tests/test_cli/test_revoke_cg_access.py @@ -3,11 +3,11 @@ import mock import six import unittest -from koji_cli.commands import handle_revoke_cg_access +from koji_cli.commands.revoke_cg_access import handle_revoke_cg_access from . import utils -class TestRevokeCGAccess(utils.CliTestCase): +class TestRevokeCgAccess(utils.CliTestCase): # Show long diffs in error output... maxDiff = None @@ -20,7 +20,7 @@ class TestRevokeCGAccess(utils.CliTestCase): """ % (self.progname, self.progname) @mock.patch('sys.stdout', new_callable=six.StringIO) - @mock.patch('koji_cli.commands.activate_session') + @mock.patch('koji_cli.commands.revoke_cg_access.activate_session') def test_handle_revoke_cg_access( self, activate_session_mock, diff --git a/tests/test_cli/test_revoke_permission.py b/tests/test_cli/test_revoke_permission.py index e532df0..cb78ce9 100644 --- a/tests/test_cli/test_revoke_permission.py +++ b/tests/test_cli/test_revoke_permission.py @@ -3,7 +3,7 @@ import mock import six import unittest -from koji_cli.commands import handle_revoke_permission +from koji_cli.commands.revoke_permission import handle_revoke_permission from . import utils @@ -20,7 +20,7 @@ class TestRevokePermission(utils.CliTestCase): """ % (self.progname, self.progname) @mock.patch('sys.stdout', new_callable=six.StringIO) - @mock.patch('koji_cli.commands.activate_session') + @mock.patch('koji_cli.commands.revoke_permission.activate_session') def test_handle_revoke_permission( self, activate_session_mock, diff --git a/tests/test_cli/test_search.py b/tests/test_cli/test_search.py index 2996c2a..bb8afdc 100644 --- a/tests/test_cli/test_search.py +++ b/tests/test_cli/test_search.py @@ -20,7 +20,7 @@ Available search types: package, build, tag, target, user, host, rpm, maven, win %s: error: {message} """ % (self.progname, self.progname) - @mock.patch('koji_cli.commands.ensure_connection') + @mock.patch('koji_cli.lib.ensure_connection') @mock.patch('sys.stdout', new_callable=six.StringIO) def test_anon_handle_search( self, diff --git a/tests/test_cli/test_set_build_volume.py b/tests/test_cli/test_set_build_volume.py index 33302f1..b65ddd1 100644 --- a/tests/test_cli/test_set_build_volume.py +++ b/tests/test_cli/test_set_build_volume.py @@ -3,7 +3,7 @@ import mock import six import unittest -from koji_cli.commands import handle_set_build_volume +from koji_cli.commands.set_build_volume import handle_set_build_volume from . import utils @@ -20,7 +20,7 @@ class TestSetBuildVolume(utils.CliTestCase): """ % (self.progname, self.progname) @mock.patch('sys.stdout', new_callable=six.StringIO) - @mock.patch('koji_cli.commands.activate_session') + @mock.patch('koji_cli.commands.set_build_volume.activate_session') def test_handle_set_build_volume(self, activate_session_mock, stdout): """Test handle_set_build_volume function""" session = mock.MagicMock() diff --git a/tests/test_cli/test_set_pkg_arches.py b/tests/test_cli/test_set_pkg_arches.py index fd4ecaf..9f71192 100644 --- a/tests/test_cli/test_set_pkg_arches.py +++ b/tests/test_cli/test_set_pkg_arches.py @@ -3,7 +3,7 @@ import mock import six import unittest -from koji_cli.commands import handle_set_pkg_arches +from koji_cli.commands.set_pkg_arches import handle_set_pkg_arches from . import utils @@ -20,7 +20,7 @@ class TestSetPkgArches(utils.CliTestCase): """ % (self.progname, self.progname) @mock.patch('sys.stdout', new_callable=six.StringIO) - @mock.patch('koji_cli.commands.activate_session') + @mock.patch('koji_cli.commands.set_pkg_arches.activate_session') def test_handle_set_pkg_arches( self, activate_session_mock, diff --git a/tests/test_cli/test_set_pkg_owner.py b/tests/test_cli/test_set_pkg_owner.py index 03bef5e..2323041 100644 --- a/tests/test_cli/test_set_pkg_owner.py +++ b/tests/test_cli/test_set_pkg_owner.py @@ -3,7 +3,7 @@ import mock import six import unittest -from koji_cli.commands import handle_set_pkg_owner +from koji_cli.commands.set_pkg_owner import handle_set_pkg_owner from . import utils @@ -20,7 +20,7 @@ class TestSetPkgOwner(utils.CliTestCase): """ % (self.progname, self.progname) @mock.patch('sys.stdout', new_callable=six.StringIO) - @mock.patch('koji_cli.commands.activate_session') + @mock.patch('koji_cli.commands.set_pkg_owner.activate_session') def test_handle_set_pkg_owner( self, activate_session_mock, diff --git a/tests/test_cli/test_set_task_priority.py b/tests/test_cli/test_set_task_priority.py index fb2c23a..9711c47 100644 --- a/tests/test_cli/test_set_task_priority.py +++ b/tests/test_cli/test_set_task_priority.py @@ -3,7 +3,7 @@ import mock import six import unittest -from koji_cli.commands import handle_set_task_priority +from koji_cli.commands.set_task_priority import handle_set_task_priority import koji from . import utils @@ -18,7 +18,7 @@ class TestSetTaskPriority(utils.CliTestCase): self.options.debug = False self.session = mock.MagicMock() self.session.getAPIVersion.return_value = koji.API_VERSION - self.activate_session_mock = mock.patch('koji_cli.commands.activate_session').start() + self.activate_session_mock = mock.patch('koji_cli.commands.set_task_priority.activate_session').start() self.error_format = """Usage: %s set-task-priority [options] --priority= [ ...] (Specify the --help global option for a list of other help options) diff --git a/tests/test_cli/test_spin_commands.py b/tests/test_cli/test_spin_commands.py index 9de23e2..51f1ee9 100644 --- a/tests/test_cli/test_spin_commands.py +++ b/tests/test_cli/test_spin_commands.py @@ -6,8 +6,10 @@ import six import mock import koji -from koji_cli.commands import handle_spin_livecd, handle_spin_livemedia, handle_spin_appliance, \ - _build_image +from koji_cli.commands.spin_livemedia import handle_spin_livemedia +from koji_cli.commands.spin_livecd import handle_spin_livecd +from koji_cli.commands.spin_appliance import handle_spin_appliance +from koji_cli.lib import _build_image from . import utils @@ -84,10 +86,10 @@ class TestBuildImage(utils.CliTestCase): self.options.poll_interval = 10 self.session = mock.MagicMock() self.arguments = ['test-image', '1', 'target', 'x86_64', 'image.ks'] - self.activate_session = mock.patch('koji_cli.commands.activate_session').start() - self.unique_path = mock.patch('koji_cli.commands.unique_path').start() - self.running_in_bg = mock.patch('koji_cli.commands._running_in_bg').start() - self.watch_tasks = mock.patch('koji_cli.commands.watch_tasks').start() + self.activate_session = mock.patch('koji_cli.lib.activate_session').start() + self.unique_path = mock.patch('koji_cli.lib.unique_path').start() + self.running_in_bg = mock.patch('koji_cli.lib._running_in_bg').start() + self.watch_tasks = mock.patch('koji_cli.lib.watch_tasks').start() self.build_target = { 'id': 1, @@ -140,7 +142,7 @@ class TestBuildImage(utils.CliTestCase): self.arguments, 'appliance'] with mock.patch('sys.stdout', new_callable=six.StringIO) as stdout: _build_image(*args) - self.assert_console_message(stdout, expected) + self.assert_console_message(stdout, expected,) self.session.buildImage.assert_called_once() self.watch_tasks.assert_called_once() @@ -262,7 +264,7 @@ class TestSpinAppliance(utils.CliTestCase): %s: error: {message} """ % (self.progname, self.progname) - @mock.patch('koji_cli.commands._build_image') + @mock.patch('koji_cli.commands.spin_appliance._build_image') def test_handle_spin_appliance(self, build_image_mock): """Test handle_spin_appliance function""" args = ['name', 'version', 'target', 'arch', 'file.ks'] @@ -279,7 +281,7 @@ class TestSpinAppliance(utils.CliTestCase): self.assert_console_message( stdout, 'spin-appliance is deprecated and will be replaced with image-build\n') - @mock.patch('koji_cli.commands._build_image') + @mock.patch('koji_cli.commands.spin_appliance._build_image') def test_handle_spin_appliance_argument_error(self, build_image_mock): """Test handle_spin_appliance function argument error""" # takes excatly 5 arguments @@ -332,7 +334,7 @@ Options: """ % (self.progname)) -class TestSpinLiveMedia(utils.CliTestCase): +class TestSpinLivemedia(utils.CliTestCase): # Show long diffs in error output... maxDiff = None @@ -347,7 +349,7 @@ class TestSpinLiveMedia(utils.CliTestCase): %s: error: {message} """ % (self.progname, self.progname) - @mock.patch('koji_cli.commands._build_image') + @mock.patch('koji_cli.commands.spin_livemedia._build_image') def test_handle_spin_livemedia(self, build_image_mock): """Test handle_spin_livemedia function""" args = ['name', 'version', 'target', 'arch', 'file.ks'] @@ -374,7 +376,7 @@ class TestSpinLiveMedia(utils.CliTestCase): stderr=expected, activate_session=None) - @mock.patch('koji_cli.commands._build_image') + @mock.patch('koji_cli.commands.spin_livemedia._build_image') def test_handle_spin_livemedia_argument_error(self, build_image_mock): """Test handle_spin_livemedia function argument error""" # takes excatly 5 arguments @@ -451,7 +453,7 @@ Options: """ % (self.progname)) -class TestSpinLiveCD(utils.CliTestCase): +class TestSpinLivecd(utils.CliTestCase): # Show long diffs in error output... maxDiff = None @@ -466,7 +468,7 @@ class TestSpinLiveCD(utils.CliTestCase): %s: error: {message} """ % (self.progname, self.progname) - @mock.patch('koji_cli.commands._build_image') + @mock.patch('koji_cli.commands.spin_livecd._build_image') def test_handle_spin_livecd(self, build_image_mock): """Test handle_spin_livecd function""" args = ['name', 'version', 'target', 'arch', 'file.ks'] @@ -482,7 +484,7 @@ class TestSpinLiveCD(utils.CliTestCase): self.assert_console_message( stdout, 'spin-livecd is deprecated and will be replaced with spin-livemedia\n') - @mock.patch('koji_cli.commands._build_image') + @mock.patch('koji_cli.commands.spin_livecd._build_image') def test_handle_spin_livecd_argument_error(self, build_image_mock): """Test handle_spin_livecd function argument error""" # takes excatly 5 arguments @@ -501,7 +503,7 @@ class TestSpinLiveCD(utils.CliTestCase): activate_session=None) build_image_mock.assert_not_called() - @mock.patch('koji_cli.commands._build_image') + @mock.patch('koji_cli.commands.spin_livecd._build_image') def test_handle_spin_livecd_longer_volid(self, build_image_mock): """Test handle_spin_livecd volid options error""" expected = self.format_error_message("Volume ID has a maximum length of 32 characters") diff --git a/tests/test_cli/test_tag_build.py b/tests/test_cli/test_tag_build.py index f7efef0..06db663 100644 --- a/tests/test_cli/test_tag_build.py +++ b/tests/test_cli/test_tag_build.py @@ -3,7 +3,7 @@ import mock import six import unittest -from koji_cli.commands import handle_tag_build +from koji_cli.commands.tag_build import handle_tag_build from . import utils @@ -22,10 +22,10 @@ class TestTagBuild(utils.CliTestCase): %s: error: {message} """ % (self.progname, self.progname) - self.activate_session = mock.patch('koji_cli.commands.activate_session').start() - self.running_in_bg = mock.patch('koji_cli.commands._running_in_bg').start() + self.activate_session = mock.patch('koji_cli.commands.tag_build.activate_session').start() + self.running_in_bg = mock.patch('koji_cli.commands.tag_build._running_in_bg').start() self.running_in_bg.return_value = False - self.watch_tasks = mock.patch('koji_cli.commands.watch_tasks').start() + self.watch_tasks = mock.patch('koji_cli.commands.tag_build.watch_tasks').start() self.watch_tasks.return_value = True def tearDown(self): diff --git a/tests/test_cli/test_taskinfo.py b/tests/test_cli/test_taskinfo.py index 3e72928..6b0296a 100644 --- a/tests/test_cli/test_taskinfo.py +++ b/tests/test_cli/test_taskinfo.py @@ -6,8 +6,8 @@ import time import unittest import koji -from koji_cli.commands import anon_handle_taskinfo, \ - _printTaskInfo, _parseTaskParams +from koji_cli.commands.taskinfo import anon_handle_taskinfo +from koji_cli.lib import _printTaskInfo, _parseTaskParams from . import utils @@ -36,7 +36,7 @@ class TestParseTaskParams(utils.CliTestCase): def test_error_with_param(self): params = [] expect = ['Unable to parse task parameters'] - with mock.patch('koji_cli.commands.logging.getLogger', create=True) as get_logger_mock: + with mock.patch('logging.getLogger', create=True) as get_logger_mock: h = get_logger_mock() h.isEnabledFor.return_value = True self.__run_parseTask_test('buildSRPMFromCVS', params, expect) @@ -358,7 +358,7 @@ class TestPrintTaskInfo(utils.CliTestCase): 'krb_principal': None } - @mock.patch('koji_cli.commands.list_task_output_all_volumes') + @mock.patch('koji_cli.lib.list_task_output_all_volumes') def test_printTaskInfo_create_repo(self, list_task_output_mock): session = mock.MagicMock() @@ -429,7 +429,7 @@ Finished: Thu Jan 1 00:50:00 1970 _printTaskInfo(session, 1, '/mnt/koji') self.assert_console_message(stdout, expected) - @mock.patch('koji_cli.commands.list_task_output_all_volumes') + @mock.patch('koji_cli.lib.list_task_output_all_volumes') def test_printTaskInfo_build_srpm(self, list_task_output_mock): session = mock.MagicMock() @@ -580,7 +580,7 @@ Build: bash-4.4.12-5.fc26 (1) self.assertEqual(str(cm.exception), "No such task: %d" % task_id) -class TestTaskInfo(utils.CliTestCase): +class TestTaskinfo(utils.CliTestCase): # Show long diffs in error output... maxDiff = None @@ -593,12 +593,10 @@ class TestTaskInfo(utils.CliTestCase): """ % (self.progname, self.progname) @mock.patch('sys.stdout', new_callable=six.StringIO) - @mock.patch('koji_cli.commands.activate_session') - @mock.patch('koji_cli.commands.ensure_connection') + @mock.patch('koji_cli.lib.ensure_connection') def test_anon_handle_taskinfo( self, ensure_connection_mock, - activate_session_mock, stdout): """Test anon_handle_taskinfo function""" session = mock.MagicMock() @@ -615,7 +613,7 @@ class TestTaskInfo(utils.CliTestCase): [], stderr=expected, activate_session=None) - activate_session_mock.assert_not_called() + ensure_connection_mock.assert_not_called() # Case 2. show task info task_output = """Task: 1 @@ -631,7 +629,7 @@ Host: kojibuilder def print_task(*args, **kwargs): print(task_output, end='') - with mock.patch('koji_cli.commands._printTaskInfo', new=print_task): + with mock.patch('koji_cli.commands.taskinfo._printTaskInfo', new=print_task): anon_handle_taskinfo(options, session, ['1']) self.assert_console_message(stdout, task_output) diff --git a/tests/test_cli/test_unblock_group_pkg.py b/tests/test_cli/test_unblock_group_pkg.py index 7ae184e..beef13c 100644 --- a/tests/test_cli/test_unblock_group_pkg.py +++ b/tests/test_cli/test_unblock_group_pkg.py @@ -2,11 +2,11 @@ from __future__ import absolute_import import mock import unittest -from koji_cli.commands import handle_unblock_group_pkg +from koji_cli.commands.unblock_group_pkg import handle_unblock_group_pkg from . import utils -class TestBlockGroupPkg(utils.CliTestCase): +class TestUnblockGroupPkg(utils.CliTestCase): # Show long diffs in error output... maxDiff = None @@ -14,7 +14,8 @@ class TestBlockGroupPkg(utils.CliTestCase): def setUp(self): self.session = mock.MagicMock() self.options = mock.MagicMock() - self.activate_session = mock.patch('koji_cli.commands.activate_session').start() + self.activate_session = \ + mock.patch('koji_cli.commands.unblock_group_pkg.activate_session').start() self.error_format = """Usage: %s unblock-group-pkg [options] [ ...] (Specify the --help global option for a list of other help options) diff --git a/tests/test_cli/test_unblock_group_req.py b/tests/test_cli/test_unblock_group_req.py index 5b20a2d..a3e4e1b 100644 --- a/tests/test_cli/test_unblock_group_req.py +++ b/tests/test_cli/test_unblock_group_req.py @@ -2,7 +2,7 @@ from __future__ import absolute_import import mock import unittest -from koji_cli.commands import handle_unblock_group_req +from koji_cli.commands.unblock_group_req import handle_unblock_group_req from . import utils @@ -14,7 +14,7 @@ class TestUnblockGroupReq(utils.CliTestCase): def setUp(self): self.session = mock.MagicMock() self.options = mock.MagicMock() - self.activate_session = mock.patch('koji_cli.commands.activate_session').start() + self.activate_session = mock.patch('koji_cli.commands.unblock_group_req.activate_session').start() self.error_format = """Usage: %s unblock-group-req [options] (Specify the --help global option for a list of other help options) diff --git a/tests/test_cli/test_unblock_notification.py b/tests/test_cli/test_unblock_notification.py index 8bd3499..d6c8ae0 100644 --- a/tests/test_cli/test_unblock_notification.py +++ b/tests/test_cli/test_unblock_notification.py @@ -4,7 +4,7 @@ import mock from six.moves import StringIO import koji -from koji_cli.commands import handle_unblock_notification +from koji_cli.commands.unblock_notification import handle_unblock_notification from . import utils @@ -15,7 +15,7 @@ class TestUnblockNotification(utils.CliTestCase): self.session = mock.MagicMock() self.session.getAPIVersion.return_value = koji.API_VERSION - @mock.patch('koji_cli.commands.activate_session') + @mock.patch('koji_cli.commands.unblock_notification.activate_session') @mock.patch('sys.stderr', new_callable=StringIO) def test_handle_unblock_notification_without_args(self, stderr, activate_session): expected = "Usage: %s unblock-notification [options] " \ @@ -30,7 +30,7 @@ class TestUnblockNotification(utils.CliTestCase): self.assert_console_message(stderr, expected) activate_session.assert_called_once_with(self.session, self.options) - @mock.patch('koji_cli.commands.activate_session') + @mock.patch('koji_cli.commands.unblock_notification.activate_session') @mock.patch('sys.stderr', new_callable=StringIO) def test_handle_unblock_notification_not_all_integers(self, stderr, activate_session): expected = "Usage: %s unblock-notification [options] " \ @@ -45,7 +45,7 @@ class TestUnblockNotification(utils.CliTestCase): self.assert_console_message(stderr, expected) activate_session.assert_called_once_with(self.session, self.options) - @mock.patch('koji_cli.commands.activate_session') + @mock.patch('koji_cli.commands.unblock_notification.activate_session') @mock.patch('sys.stdout', new_callable=StringIO) def test_handle_unblock_notification(self, stdout, activate_session): self.options.quiet = False @@ -58,7 +58,7 @@ class TestUnblockNotification(utils.CliTestCase): print(actual) self.assertMultiLineEqual(actual, expected) - @mock.patch('koji_cli.commands.activate_session') + @mock.patch('koji_cli.commands.unblock_notification.activate_session') @mock.patch('sys.stdout', new_callable=StringIO) def test_handle_unblock_notification_quiet(self, stdout, activate_session): expected = "" diff --git a/tests/test_cli/test_unblock_pkg.py b/tests/test_cli/test_unblock_pkg.py index 1fb25d1..3d62c8a 100644 --- a/tests/test_cli/test_unblock_pkg.py +++ b/tests/test_cli/test_unblock_pkg.py @@ -4,7 +4,7 @@ import six import unittest -from koji_cli.commands import handle_unblock_pkg +from koji_cli.commands.unblock_pkg import handle_unblock_pkg from . import utils @@ -21,7 +21,7 @@ class TestUnblockPkg(utils.CliTestCase): """ % (self.progname, self.progname) @mock.patch('sys.stdout', new_callable=six.StringIO) - @mock.patch('koji_cli.commands.activate_session') + @mock.patch('koji_cli.commands.unblock_pkg.activate_session') def test_handle_unblock_pkg( self, activate_session_mock, diff --git a/tests/test_cli/test_untag_build.py b/tests/test_cli/test_untag_build.py index 2fa7fb9..f2167b2 100644 --- a/tests/test_cli/test_untag_build.py +++ b/tests/test_cli/test_untag_build.py @@ -4,7 +4,7 @@ import mock from six.moves import StringIO import koji -from koji_cli.commands import handle_untag_build +from koji_cli.commands.untag_build import handle_untag_build from . import utils @@ -14,7 +14,7 @@ class TestUntagBuild(utils.CliTestCase): self.options.debug = False self.session = mock.MagicMock() self.session.getAPIVersion.return_value = koji.API_VERSION - self.activate_session_mock = mock.patch('koji_cli.commands.activate_session').start() + self.activate_session_mock = mock.patch('koji_cli.commands.untag_build.activate_session').start() self.tag_info = {'arches': 'x86_64', 'extra': {}, 'id': 1, diff --git a/tests/test_cli/test_userinfo.py b/tests/test_cli/test_userinfo.py index 5860faf..8a26a6a 100644 --- a/tests/test_cli/test_userinfo.py +++ b/tests/test_cli/test_userinfo.py @@ -56,7 +56,7 @@ class TestUserinfo(utils.CliTestCase): self.assert_console_message(stderr, expected_warn) @mock.patch('sys.stdout', new_callable=StringIO) - @mock.patch('koji_cli.commands.ensure_connection') + @mock.patch('koji_cli.lib.ensure_connection') def test_userinfo(self, ensure_connection, stdout): expected = """User name: test-user User ID: 1 diff --git a/tests/test_cli/test_wait_repo.py b/tests/test_cli/test_wait_repo.py index 25fe82b..51ffdae 100644 --- a/tests/test_cli/test_wait_repo.py +++ b/tests/test_cli/test_wait_repo.py @@ -4,11 +4,12 @@ from __future__ import print_function import unittest import copy +from koji_cli.lib import activate_session import mock import six -from koji_cli.commands import anon_handle_wait_repo +from koji_cli.commands.wait_repo import anon_handle_wait_repo from . import utils @@ -53,8 +54,7 @@ class TestWaitRepo(utils.CliTestCase): self.setUpMocks() def setUpMocks(self): - self.activate_session = mock.patch('koji_cli.commands.activate_session').start() - self.ensure_connection = mock.patch('koji_cli.commands.ensure_connection').start() + self.ensure_connection = mock.patch('koji_cli.commands.wait_repo.ensure_connection').start() self.checkForBuilds = mock.patch('koji.util.checkForBuilds').start() def tearDown(self): @@ -195,7 +195,7 @@ class TestWaitRepo(utils.CliTestCase): test[0], stderr=self.format_error_message(test[1]), activate_session=None) - self.activate_session.assert_not_called() + self.assertEqual(len(self.ensure_connection.mock_calls), 2) @mock.patch('sys.stderr', new_callable=six.StringIO) def test_anon_handle_wait_repo_target_not_found(self, stderr): diff --git a/tests/test_cli/test_win_build.py b/tests/test_cli/test_win_build.py index 9aeac74..90fd91d 100644 --- a/tests/test_cli/test_win_build.py +++ b/tests/test_cli/test_win_build.py @@ -4,7 +4,7 @@ import mock from six.moves import StringIO import koji -from koji_cli.commands import handle_win_build +from koji_cli.commands.win_build import handle_win_build from . import utils diff --git a/tests/test_cli/test_wrapper_rpm.py b/tests/test_cli/test_wrapper_rpm.py index bb40cb2..f721cde 100644 --- a/tests/test_cli/test_wrapper_rpm.py +++ b/tests/test_cli/test_wrapper_rpm.py @@ -3,7 +3,7 @@ import mock import six import unittest -from koji_cli.commands import handle_wrapper_rpm +from koji_cli.commands.wrapper_rpm import handle_wrapper_rpm from . import utils @@ -26,9 +26,9 @@ class TestWrapperRpm(utils.CliTestCase): @mock.patch('sys.stdout', new_callable=six.StringIO) @mock.patch('sys.stderr', new_callable=six.StringIO) - @mock.patch('koji_cli.commands._running_in_bg') - @mock.patch('koji_cli.commands.watch_tasks') - @mock.patch('koji_cli.commands.activate_session') + @mock.patch('koji_cli.commands.wrapper_rpm._running_in_bg') + @mock.patch('koji_cli.commands.wrapper_rpm.watch_tasks') + @mock.patch('koji_cli.commands.wrapper_rpm.activate_session') def test_handle_wrapper_rpm( self, activate_session_mock, @@ -83,9 +83,9 @@ class TestWrapperRpm(utils.CliTestCase): @mock.patch('sys.stdout', new_callable=six.StringIO) @mock.patch('sys.stderr', new_callable=six.StringIO) @mock.patch('koji.util.parse_maven_param') - @mock.patch('koji_cli.commands._running_in_bg') - @mock.patch('koji_cli.commands.watch_tasks') - @mock.patch('koji_cli.commands.activate_session') + @mock.patch('koji_cli.commands.wrapper_rpm._running_in_bg') + @mock.patch('koji_cli.commands.wrapper_rpm.watch_tasks') + @mock.patch('koji_cli.commands.wrapper_rpm.activate_session') def test_handle_wrapper_rpm_with_ini_config( self, activate_session_mock, @@ -193,7 +193,7 @@ class TestWrapperRpm(utils.CliTestCase): @mock.patch('sys.stdout', new_callable=six.StringIO) @mock.patch('sys.stderr', new_callable=six.StringIO) - @mock.patch('koji_cli.commands.activate_session') + @mock.patch('koji_cli.commands.wrapper_rpm.activate_session') def test_handle_wrapper_rpm_argument_error( self, activate_session_mock, stderr, stdout): """Test handle_wrapper_rpm help message output""" diff --git a/tests/test_cli/test_write_signed_rpm.py b/tests/test_cli/test_write_signed_rpm.py index 6d1edee..37abfc5 100644 --- a/tests/test_cli/test_write_signed_rpm.py +++ b/tests/test_cli/test_write_signed_rpm.py @@ -6,7 +6,7 @@ import unittest from mock import call import koji -from koji_cli.commands import handle_write_signed_rpm +from koji_cli.commands.write_signed_rpm import handle_write_signed_rpm from . import utils import os @@ -111,7 +111,7 @@ class TestWriteSignedRPM(utils.CliTestCase): """ % (self.progname, self.progname) @mock.patch('sys.stdout', new_callable=six.StringIO) - @mock.patch('koji_cli.commands.activate_session') + @mock.patch('koji_cli.commands.write_signed_rpm.activate_session') def test_handle_write_signed_rpm( self, activate_session_mock, diff --git a/tests/test_cli/utils.py b/tests/test_cli/utils.py index 60d3cf6..df58538 100644 --- a/tests/test_cli/utils.py +++ b/tests/test_cli/utils.py @@ -2,6 +2,7 @@ from __future__ import print_function from __future__ import absolute_import import mock import os +import re import six import sys from six.moves import map @@ -66,6 +67,11 @@ class CliTestCase(unittest.TestCase): kwargs['file'] = self.STDOUT print(" ".join(map(str, args)), **kwargs) + def handler_name(self): + clsname = self.__class__.__name__[4:] + handler = re.sub(r'([A-Z])', r'_\1', clsname).lower()[1:] + return handler + def assert_function_wrapper(self, callableObj, *args, **kwargs): """Wrapper func with anonymous funtion without argument""" self.__assert_callable(callableObj) @@ -98,7 +104,7 @@ class CliTestCase(unittest.TestCase): Keyword arguments: (reseverd for assert_system_exit) activate_session (string): - Mock koji_cli.commands.activate_session and test if it is + Mock koji_cli.commands..activate_session and test if it is called. Default is on, use None to stop mocking. @@ -136,7 +142,7 @@ class CliTestCase(unittest.TestCase): ] activate = kwargs.get( - 'activate_session', 'koji_cli.commands.activate_session') + 'activate_session', 'koji_cli.commands.%s.activate_session' % self.handler_name()) # stdout/stderr message comparison, None means don't care # message/error allows many different data types, None, string and dict @@ -189,8 +195,12 @@ class CliTestCase(unittest.TestCase): self.assert_console_message(stderr, **message['stderr']) assert_function() - @mock.patch('koji_cli.commands.activate_session') - def assert_help(self, callableObj, message, activate_session_mock): + def assert_help(self, callableObj, message): + patch = mock.patch('koji_cli.commands.%s.activate_session' % self.handler_name()) + try: + activate_session_mock = patch.start() + except Exception: + activate_session_mock = None # optarse uses gettext directly and it is driven by LANGUAGE # we need english to get comparable strings os.environ['LANGUAGE'] = 'C' @@ -203,7 +213,9 @@ class CliTestCase(unittest.TestCase): stderr='', activate_session=None, exit_code=0) - activate_session_mock.assert_not_called() + if activate_session_mock is not None: + activate_session_mock.assert_not_called() + activate_session_mock.stop() def assertExitCode(self, ex, code): if isinstance(ex.exception, int): From f9b7852b6f3704ab7acce5de90c0676b3426dc97 Mon Sep 17 00:00:00 2001 From: Tester Date: Jun 13 2022 14:59:31 +0000 Subject: [PATCH 6/6] remove splitting script Related: https://pagure.io/koji/issue/2116 --- diff --git a/cli/koji_cli/split.py b/cli/koji_cli/split.py deleted file mode 100755 index 4a039f0..0000000 --- a/cli/koji_cli/split.py +++ /dev/null @@ -1,132 +0,0 @@ -#!/usr/bin/python3 -import os -import shutil - -HEAD = """from __future__ import absolute_import, division - -import ast -import fnmatch -import itertools -import hashlib -import json -import logging -import os -import pprint -import random -import re -import stat -import sys -import textwrap -import time -import traceback -from collections import OrderedDict, defaultdict -from datetime import datetime -from dateutil.tz import tzutc -from optparse import SUPPRESS_HELP, OptionParser - -import six -import six.moves.xmlrpc_client -from six.moves import filter, map, range, zip - -import koji -from koji.util import base64encode, md5_constructor, to_list - -from koji_cli.lib import ( - TimeOption, - DatetimeJSONEncoder, - _list_tasks, - _progress_callback, - _running_in_bg, - activate_session, - arg_filter, - download_archive, - download_file, - download_rpm, - ensure_connection, - error, - format_inheritance_flags, - get_usage_str, - greetings, - linked_upload, - list_task_output_all_volumes, - print_task_headers, - print_task_recurse, - unique_path, - warn, - watch_logs, - watch_tasks, - truncate_string -) - - -""" - -INIT = """import importlib -import inspect -import pkgutil -import six - -__all__ = [] - -for loader, name, is_pkg in pkgutil.walk_packages(__path__): - if six.PY2: - module = loader.find_module(name).load_module(name) - else: - spec = loader.find_spec(name) - module = importlib.util.module_from_spec(spec) - spec.loader.exec_module(module) - - for name, entry in inspect.getmembers(module): - if name.startswith('handle_') or name.startswith('anon_handle_'): - globals()[name] = entry - __all__.append(name) -""" - -try: - shutil.rmtree('commands') -except IOError: - pass -os.mkdir('commands') - -with open('commands/__init__.py', 'wt') as f: - f.write(INIT) - -out = open('xyz.py', 'wt') -out_cmd = None - -state = 'out' -for line in open('c.py'): - if state == 'out': - if line.startswith('def '): - fname = line[4:].split('(')[0] - if fname.startswith('handle_') or fname.startswith('anon_handle_'): - if fname.startswith('handle_'): - fn = f'{fname[7:]}' - else: - fn = f'{fname[12:]}' - out_cmd = open(os.path.join('commands', "%s.py" % fn), 'wt') - out_cmd.write(HEAD) - out_cmd.write(line) - state = 'in' - else: - out.write(line) - else: - out.write(line) - elif state == 'in': - if line.startswith('def '): - fname = line[4:].split('(')[0] - if fname.startswith('handle_') or fname.startswith('anon_handle_'): - if fname.startswith('handle_'): - fn = f'{fname[7:]}' - else: - fn = f'{fname[12:]}' - out_cmd = open(os.path.join('commands', "%s.py" % fn), 'wt') - out_cmd.write(HEAD) - out_cmd.write(line) - state = 'in' - else: - out_cmd.write(line) - else: - out_cmd.write(line) - -# run autoflake -i -r --remove-all-unused-imports to clean up headers