#199 CLI plugins
Merged 6 years ago by mikem. Opened 7 years ago by tkopecek.
tkopecek/koji issue193  into  master

file modified
+2 -2
@@ -66,7 +66,7 @@ 

  

  test:

  	coverage erase

- 	PYTHONPATH=hub/.:plugins/hub/.:plugins/builder/. coverage run \

+ 	PYTHONPATH=hub/.:plugins/hub/.:plugins/builder/.:plugins/cli/.:cli/. coverage run \

  	    --source . /usr/bin/nosetests

  	coverage report

  	coverage html
@@ -74,7 +74,7 @@ 

  

  test3:

  	coverage erase

- 	PYTHONPATH=hub/.:plugins/hub/.:plugins/builder/. coverage3 run \

+ 	PYTHONPATH=hub/.:plugins/hub/.:plugins/builder/.:plugins/cli/.:cli/. coverage3 run \

  	    --rcfile .coveragerc3 --source . \

  	    /usr/bin/nosetests-3 \

  	    tests/test_lib tests/test_cli

file modified
+13
@@ -1,3 +1,12 @@ 

+ SUBDIRS=koji_cli

+ 

+ PYTHON=python

+ PACKAGE = $(shell basename `pwd`)

+ PYVER := $(shell $(PYTHON) -c 'import sys; print("%.3s" %(sys.version))')

+ PYSYSDIR := $(shell $(PYTHON) -c 'import sys; print(sys.prefix)')

+ PYLIBDIR = $(PYSYSDIR)/lib/python$(PYVER)

+ PKGDIR = $(PYLIBDIR)/site-packages

+ 

  FILES = koji

  

  _default:
@@ -13,7 +22,11 @@ 

  		exit 1; \

  	fi

  

+ 	for d in $(SUBDIRS); do make DESTDIR=$(DESTDIR) \

+ 		-C $$d install; [ $$? = 0 ] || exit 1; done

+ 

  	mkdir -p $(DESTDIR)/usr/bin

  	install -p -m 755 $(FILES) $(DESTDIR)/usr/bin

  	mkdir -p $(DESTDIR)/etc/koji.conf.d

  	install -p -m 644 koji.conf $(DESTDIR)/etc/koji.conf

+ 	install -p -m 644 koji.conf $(DESTDIR)/etc/koji.conf

file modified
+122 -7501
@@ -26,143 +26,62 @@ 

  

  from __future__ import absolute_import

  from __future__ import division

- import sys

- from six.moves import range

- from six.moves import zip

- import six

- from six.moves import filter

- from six.moves import map

- try:

-     import krbV

- except ImportError:  # pragma: no cover

-     krbV = None

- try:

-     import ast

- except ImportError:  # pragma: no cover

-     ast = None

- try:

-     import json

- except ImportError:  # pragma: no cover

-     try:

-         import simplejson as json

-     except ImportError:

-         json = None

- import six.moves.configparser

- import base64

- import dateutil.parser

- import errno

- import koji

- import koji.util

- import fnmatch

- from koji.util import md5_constructor

  import logging

+ import optparse

  import os

  import re

- import pprint

- import pycurl

- import random

- import socket

- import stat

- import string

- import time

- import traceback

- import six.moves.xmlrpc_client

- try:

-     import libcomps

- except ImportError:  # pragma: no cover

-     libcomps = None

-     try:

-         import yum.comps as yumcomps

-     except ImportError:

-         yumcomps = None

- import optparse

+ import six

+ import sys

+ import types

  

+ import six.moves.configparser

+ import six.moves.xmlrpc_client

  

- # fix OptionParser for python 2.3 (optparse verion 1.4.1+)

- # code taken from optparse version 1.5a2

- OptionParser = optparse.OptionParser

- if optparse.__version__ == "1.4.1+":  # pragma: no cover

-     def _op_error(self, msg):

-         self.print_usage(sys.stderr)

-         msg = "%s: error: %s\n" % (self._get_prog_name(), msg)

-         if msg:

-             sys.stderr.write(msg)

-         sys.exit(2)

-     OptionParser.error = _op_error

+ import koji

+ import koji.util

+ import koji.plugin

  

- greetings = ('hello', 'hi', 'yo', "what's up", "g'day", 'back to work',

-              'bonjour',

-              'hallo',

-              'ciao',

-              'hola',

-             u'olá',

-             u'dobrý den',

-             u'zdravstvuite',

-             u'góðan daginn',

-              'hej',

-              'tervehdys',

-             u'grüezi',

-             u'céad míle fáilte',

-             u'hylô',

-             u'bună ziua',

-             u'jó napot',

-              'dobre dan',

-             u'你好',

-             u'こんにちは',

-             u'नमस्कार',

-             u'안녕하세요')

+ from koji_cli.lib import _, OptionParser, get_epilog_str, greetings, \

+         warn, categories

+ from koji_cli.commands import *

  

- def _(args):

-     """Stub function for translation"""

-     return args

  

- def _printable_unicode(s):

-     if six.PY2:

-         return s.encode('utf-8')

-     else:

-         return s

+ def register_plugin(plugin):

+     """Scan a given plugin for handlers

  

- ARGMAP = {'None': None,

-           'True': True,

-           'False': False}

+     Handlers are functions marked with one of the decorators defined in koji.plugin

+     """

+     for v in vars(plugin).itervalues():

+         if isinstance(v, (types.ClassType, types.TypeType)):

+             #skip classes

+             continue

+         if callable(v):

+             if getattr(v, 'exported_cli', False):

+                 if hasattr(v, 'export_alias'):

+                     name = getattr(v, 'export_alias')

+                 else:

+                     name = v.__name__

+                 # copy object to local namespace

+                 globals()[name] = v

  

- def arg_filter(arg):

-     try:

-         return int(arg)

-     except ValueError:

-         pass

-     try:

-         return float(arg)

-     except ValueError:

-         pass

-     if arg in ARGMAP:

-         return ARGMAP[arg]

-     #handle lists/dicts?

-     return arg

  

- categories = {

-     'admin' : 'admin commands',

-     'build' : 'build commands',

-     'search' : 'search commands',

-     'download' : 'download commands',

-     'monitor'  : 'monitor commands',

-     'info' : 'info commands',

-     'bind' : 'bind commands',

-     'misc' : 'miscellaneous commands',

- }

+ def load_plugins(options):

+     """Load plugins specified by our configuration plus system plugins. Order

+     is that system plugins are first, so they can be overriden by

+     user-specified ones with same name."""

+     logger = logging.getLogger('koji.plugins')

+     syspath = '%s/lib/python%s.%s/site-packages/koji_cli_plugins' % \

+               (sys.prefix, sys.version_info.major, sys.version_info.minor)

+     if os.path.exists(syspath):

+         tracker = koji.plugin.PluginTracker(path=syspath)

+         for name in sorted(os.listdir(syspath)):

+             if not name.endswith('.py'):

+                 continue

+             name = name[:-3]

+             logger.info('Loading plugin: %s', name)

+             tracker.load(name)

+             register_plugin(tracker.get(name))

  

- def get_epilog_str(progname=None):

-     if progname is None:

-         progname = os.path.basename(sys.argv[0]) or 'koji'

-     categories_ordered=', '.join(sorted(['all'] + list(categories.keys())))

-     epilog_str = '''

- Try "%(progname)s --help" for help about global options

- Try "%(progname)s help" to get all available commands

- Try "%(progname)s <command> --help" for help about the options of a particular command

- Try "%(progname)s help <category>" to get commands under a particular category

- Available categories are: %(categories)s

- ''' % ({'progname': progname, 'categories': categories_ordered})

-     return _(epilog_str)

  

  def get_options():

      """process options from command line and config file"""
@@ -210,42 +129,6 @@ 

      parser.add_option("--help-commands", action="store_true", default=False, help=_("list commands"))

      (options, args) = parser.parse_args()

  

-     if options.help_commands:

-         list_commands()

-         sys.exit(0)

-     if not args:

-         list_commands()

-         sys.exit(0)

- 

-     aliases = {

-         'cancel-task' : 'cancel',

-         'cxl' : 'cancel',

-         'list-commands' : 'help',

-         'move-pkg': 'move-build',

-         'move': 'move-build',

-         'latest-pkg': 'latest-build',

-         'tag-pkg': 'tag-build',

-         'tag': 'tag-build',

-         'untag-pkg': 'untag-build',

-         'untag': 'untag-build',

-         'watch-tasks': 'watch-task',

-     }

-     cmd = args[0]

-     cmd = aliases.get(cmd, cmd)

-     if cmd.lower() in greetings:

-         cmd = "moshimoshi"

-     cmd = cmd.replace('-', '_')

-     if ('anon_handle_' + cmd) in globals():

-         if not options.force_auth and '--mine' not in args:

-             options.noauth = True

-         cmd = 'anon_handle_' + cmd

-     elif ('handle_' + cmd) in globals():

-         cmd = 'handle_' + cmd

-     else:

-         list_commands()

-         parser.error('Unknown command: %s' % args[0])

-         assert False  # pragma: no cover

- 

      # load local config

      try:

          result = koji.read_config(options.profile, user_config=options.configFile)
@@ -282,7364 +165,102 @@ 

              else:

                  warn("Warning: The pkgurl option is obsolete, please use topurl instead")

  

-     return options, cmd, args[1:]

- 

- def ensure_connection(session):

-     try:

-         ret = session.getAPIVersion()

-     except six.moves.xmlrpc_client.ProtocolError:

-         error(_("Error: Unable to connect to server"))

-     if ret != koji.API_VERSION:

-         warn(_("WARNING: The server is at API version %d and the client is at %d" % (ret, koji.API_VERSION)))

- 

- def print_task_headers():

-     """Print the column headers"""

-     print("ID       Pri  Owner                State    Arch       Name")

- 

- def print_task(task,depth=0):

-     """Print a task"""

-     task = task.copy()

-     task['state'] = koji.TASK_STATES.get(task['state'],'BADSTATE')

-     fmt = "%(id)-8s %(priority)-4s %(owner_name)-20s %(state)-8s %(arch)-10s "

-     if depth:

-         indent = "  "*(depth-1) + " +"

-     else:

-         indent = ''

-     label = koji.taskLabel(task)

-     print(''.join([fmt % task, indent, label]))

- 

- def print_task_recurse(task,depth=0):

-     """Print a task and its children"""

-     print_task(task,depth)

-     for child in task.get('children',()):

-         print_task_recurse(child,depth+1)

- 

- def parse_arches(arches, to_list=False):

-     """Parse comma or space-separated list of arches and return

-        only space-separated one."""

-     arches = arches.replace(',', ' ').split()

-     if to_list:

-         return arches

-     else:

-         return ' '.join(arches)

- 

- class TaskWatcher(object):

- 

-     def __init__(self,task_id,session,level=0,quiet=False):

-         self.id = task_id

-         self.session = session

-         self.info = None

-         self.level = level

-         self.quiet = quiet

- 

-     #XXX - a bunch of this stuff needs to adapt to different tasks

- 

-     def str(self):

-         if self.info:

-             label = koji.taskLabel(self.info)

-             return "%s%d %s" % ('  ' * self.level, self.id, label)

-         else:

-             return "%s%d" % ('  ' * self.level, self.id)

- 

-     def __str__(self):

-         return self.str()

- 

-     def get_failure(self):

-         """Print infomation about task completion"""

-         if self.info['state'] != koji.TASK_STATES['FAILED']:

-             return ''

-         error = None

-         try:

-             result = self.session.getTaskResult(self.id)

-         except (six.moves.xmlrpc_client.Fault,koji.GenericError) as e:

-             error = e

-         if error is None:

-             # print("%s: complete" % self.str())

-             # We already reported this task as complete in update()

-             return ''

-         else:

-             return '%s: %s' % (error.__class__.__name__, str(error).strip())

- 

-     def update(self):

-         """Update info and log if needed.  Returns True on state change."""

-         if self.is_done():

-             # Already done, nothing else to report

-             return False

-         last = self.info

-         self.info = self.session.getTaskInfo(self.id, request=True)

-         if self.info is None:

-             if not self.quiet:

-                 print("No such task id: %i" % self.id)

-             sys.exit(1)

-         state = self.info['state']

-         if last:

-             #compare and note status changes

-             laststate = last['state']

-             if laststate != state:

-                 if not self.quiet:

-                     print("%s: %s -> %s" % (self.str(), self.display_state(last), self.display_state(self.info)))

-                 return True

-             return False

-         else:

-             # First time we're seeing this task, so just show the current state

-             if not self.quiet:

-                 print("%s: %s" % (self.str(), self.display_state(self.info)))

-             return False

- 

-     def is_done(self):

-         if self.info is None:

-             return False

-         state = koji.TASK_STATES[self.info['state']]

-         return (state in ['CLOSED','CANCELED','FAILED'])

- 

-     def is_success(self):

-         if self.info is None:

-             return False

-         state = koji.TASK_STATES[self.info['state']]

-         return (state == 'CLOSED')

- 

-     def display_state(self, info):

-         # We can sometimes be passed a task that is not yet open, but

-         # not finished either.  info would be none.

-         if not info:

-             return 'unknown'

-         if info['state'] == koji.TASK_STATES['OPEN']:

-             if info['host_id']:

-                 host = self.session.getHost(info['host_id'])

-                 return 'open (%s)' % host['name']

-             else:

-                 return 'open'

-         elif info['state'] == koji.TASK_STATES['FAILED']:

-             return 'FAILED: %s' % self.get_failure()

-         else:

-             return koji.TASK_STATES[info['state']].lower()

- 

- def display_tasklist_status(tasks):

-     free = 0

-     open = 0

-     failed = 0

-     done = 0

-     for task_id in tasks.keys():

-         status = tasks[task_id].info['state']

-         if status == koji.TASK_STATES['FAILED']:

-             failed += 1

-         elif status == koji.TASK_STATES['CLOSED'] or status == koji.TASK_STATES['CANCELED']:

-             done += 1

-         elif status == koji.TASK_STATES['OPEN'] or status == koji.TASK_STATES['ASSIGNED']:

-             open += 1

-         elif status == koji.TASK_STATES['FREE']:

-             free += 1

-     print("  %d free  %d open  %d done  %d failed" % (free, open, done, failed))

- 

- def display_task_results(tasks):

-     for task in [task for task in tasks.values() if task.level == 0]:

-         state = task.info['state']

-         task_label = task.str()

- 

-         if state == koji.TASK_STATES['CLOSED']:

-             print('%s completed successfully' % task_label)

-         elif state == koji.TASK_STATES['FAILED']:

-             print('%s failed' % task_label)

-         elif state == koji.TASK_STATES['CANCELED']:

-             print('%s was canceled' % task_label)

-         else:

-             # shouldn't happen

-             print('%s has not completed' % task_label)

- 

- def watch_tasks(session,tasklist,quiet=False):

-     global options

-     if not tasklist:

-         return

-     if not quiet:

-         print("Watching tasks (this may be safely interrupted)...")

-     sys.stdout.flush()

-     rv = 0

-     try:

-         tasks = {}

-         for task_id in tasklist:

-             tasks[task_id] = TaskWatcher(task_id,session,quiet=quiet)

-         while True:

-             all_done = True

-             for task_id, task in list(tasks.items()):

-                 changed = task.update()

-                 if not task.is_done():

-                     all_done = False

-                 else:

-                     if changed:

-                         # task is done and state just changed

-                         if not quiet:

-                             display_tasklist_status(tasks)

-                     if not task.is_success():

-                         rv = 1

-                 for child in session.getTaskChildren(task_id):

-                     child_id = child['id']

-                     if not child_id in list(tasks.keys()):

-                         tasks[child_id] = TaskWatcher(child_id, session, task.level + 1, quiet=quiet)

-                         tasks[child_id].update()

-                         # If we found new children, go through the list again,

-                         # in case they have children also

-                         all_done = False

-             if all_done:

-                 if not quiet:

-                     print('')

-                     display_task_results(tasks)

-                 break

- 

-             sys.stdout.flush()

-             time.sleep(options.poll_interval)

-     except KeyboardInterrupt:

-         if tasks and not quiet:

-             progname = os.path.basename(sys.argv[0]) or 'koji'

-             tlist = ['%s: %s' % (t.str(), t.display_state(t.info))

-                             for t in tasks.values() if not t.is_done()]

-             print( \

- """Tasks still running. You can continue to watch with the '%s watch-task' command.

- Running Tasks:

- %s""" % (progname, '\n'.join(tlist)))

-         raise

-     return rv

- 

- def watch_logs(session, tasklist, opts):

-     global options

-     print("Watching logs (this may be safely interrupted)...")

-     def _isDone(session, taskId):

-         info = session.getTaskInfo(taskId)

-         if info is None:

-             print("No such task id: %i" % taskId)

-             sys.exit(1)

-         state = koji.TASK_STATES[info['state']]

-         return (state in ['CLOSED','CANCELED','FAILED'])

- 

-     offsets = {}

-     for task_id in tasklist:

-         offsets[task_id] = {}

- 

-     lastlog = None

-     while True:

-         for task_id in tasklist[:]:

-             if _isDone(session, task_id):

-                 tasklist.remove(task_id)

- 

-             output = list_task_output_all_volumes(session, task_id)

-             # convert to list of (file, volume)

-             files = []

-             for filename, volumes in six.iteritems(output):

-                 files += [(filename, volume) for volume in volumes]

- 

-             if opts.log:

-                 logs = [file_volume for file_volume in files if file_volume[0] == opts.log]

-             else:

-                 logs = [file_volume for file_volume in files if file_volume[0].endswith('log')]

- 

-             taskoffsets = offsets[task_id]

-             for log, volume in logs:

-                 contents = 'placeholder'

-                 while contents:

-                     if (log, volume) not in taskoffsets:

-                         taskoffsets[(log, volume)] = 0

- 

-                     contents = session.downloadTaskOutput(task_id, log, taskoffsets[(log, volume)], 16384, volume=volume)

-                     taskoffsets[(log, volume)] += len(contents)

-                     if contents:

-                         currlog = "%d:%s:%s:" % (task_id, volume, log)

-                         if currlog != lastlog:

-                             if lastlog:

-                                 sys.stdout.write("\n")

-                             sys.stdout.write("==> %s <==\n" % currlog)

-                             lastlog = currlog

-                         sys.stdout.write(contents)

- 

-         if not tasklist:

-             break

- 

-         time.sleep(options.poll_interval)

- 

- 

- def list_task_output_all_volumes(session, task_id):

-     """List task output with all volumes, or fake it"""

-     try:

-         return session.listTaskOutput(task_id, all_volumes=True)

-     except koji.GenericError as e:

-         if 'got an unexpected keyword argument' not in str(e):

-             raise

-     # otherwise leave off the option and fake it

-     output = session.listTaskOutput(task_id)

-     return dict([fn, ['DEFAULT']] for fn in output)

- 

- 

- def handle_add_group(options, session, args):

-     "[admin] Add a group to a tag"

-     usage = _("usage: %prog add-group <tag> <group>")

-     usage += _("\n(Specify the --help global option for a list of other help options)")

-     parser = OptionParser(usage=usage)

-     (options, args) = parser.parse_args(args)

-     if len(args) != 2:

-         parser.error(_("Please specify a tag name and a group name"))

-         assert False  # pragma: no cover

-     tag = args[0]

-     group = args[1]

- 

-     activate_session(session)

-     if not session.hasPerm('admin'):

-         print("This action requires admin privileges")

-         return 1

- 

-     dsttag = session.getTag(tag)

-     if not dsttag:

-         print("Unknown tag: %s" % tag)

-         return 1

- 

-     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:

-         print("Group %s already exists for tag %s" % (group, tag))

-         return 1

- 

-     session.groupListAdd(tag, group)

- 

- def handle_assign_task(options, session, args):

-     "[admin] Assign a task to a host"

-     usage = _('usage: %prog assign-task task_id hostname')

-     usage += _('\n(Specify the --help global option for a list of other help options)')

-     parser = OptionParser(usage=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

+     load_plugins(options)

  

-     activate_session(session)

-     if not session.hasPerm('admin'):

-         print("This action requires admin privileges")

-         return 1

- 

-     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(options, session, args):

-     "[admin] Add a host"

-     usage = _("usage: %prog add-host [options] hostname arch [arch2 ...]")

-     usage += _("\n(Specify the --help global option for a list of other help options)")

-     parser = OptionParser(usage=usage)

-     parser.add_option("--krb-principal", help=_("set a non-default kerberos principal for the host"))

-     (options, args) = parser.parse_args(args)

-     if len(args) < 2:

-         parser.error(_("Please specify a hostname and at least one arch"))

-         assert False  # pragma: no cover

-     host = args[0]

-     activate_session(session)

-     id = session.getHost(host)

-     if id:

-         print("%s is already in the database" % host)

-         return 1

-     else:

-         kwargs = {}

-         if options.krb_principal is not None:

-             kwargs['krb_principal'] = options.krb_principal

-         id = session.addHost(host, args[1:], **kwargs)

-         if id:

-             print("%s added: id %d" % (host, id))

- 

- def handle_edit_host(options, session, args):

-     "[admin] Edit a host"

-     usage = _("usage: %prog edit-host hostname ... [options]")

-     usage += _("\n(Specify the --help global option for a list of other help options)")

-     parser = OptionParser(usage=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 options.help_commands:

+         list_commands()

+         sys.exit(0)

      if not args:

-         parser.error(_("Please specify a hostname"))

- 

-     activate_session(session)

- 

-     vals = {}

-     for key, val in subopts.__dict__.items():

-         if val is not None:

-             vals[key] = val

-     if 'arches' in vals:

-         vals['arches'] = parse_arches(vals['arches'])

- 

-     session.multicall = True

-     for host in args:

-         session.getHost(host)

-     error = False

-     for host, [info] in zip(args, session.multiCall(strict=True)):

-         if not info:

-             print(_("Host %s does not exist") % host)

-             error = True

- 

-     if error:

-         print(_("No changes made, please correct the command line"))

-         return 1

- 

-     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(options, session, args):

-     "[admin] Add a host to a channel"

-     usage = _("usage: %prog add-host-to-channel [options] hostname channel")

-     usage += _("\n(Specify the --help global option for a list of other help options)")

-     parser = OptionParser(usage=usage)

-     parser.add_option("--list", action="store_true", help=optparse.SUPPRESS_HELP)

-     parser.add_option("--new", action="store_true", help=_("Create channel if needed"))

-     (options, args) = parser.parse_args(args)

-     if not options.list and len(args) != 2:

-         parser.error(_("Please specify a hostname and a channel"))

-         assert False  # pragma: no cover

-     activate_session(session)

-     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:

-             print("No such channel: %s" % channel)

-             return 1

-     host = args[0]

-     hostinfo = session.getHost(host)

-     if not hostinfo:

-         print("No such host: %s" % host)

-         return 1

-     kwargs = {}

-     if options.new:

-         kwargs['create'] = True

-     session.addHostToChannel(host, channel, **kwargs)

- 

- def handle_remove_host_from_channel(options, session, args):

-     "[admin] Remove a host from a channel"

-     usage = _("usage: %prog remove-host-from-channel [options] hostname channel")

-     usage += _("\n(Specify the --help global option for a list of other help options)")

-     parser = OptionParser(usage=usage)

-     (options, args) = parser.parse_args(args)

-     if len(args) != 2:

-         parser.error(_("Please specify a hostname and a channel"))

-         assert False  # pragma: no cover

-     host = args[0]

-     activate_session(session)

-     hostinfo = session.getHost(host)

-     if not hostinfo:

-         print("No such host: %s" % host)

-         return 1

-     hostchannels = [c['name'] for c in session.listChannels(hostinfo['id'])]

- 

-     channel = args[1]

-     if channel not in hostchannels:

-         print("Host %s is not a member of channel %s" % (host, channel))

-         return 1

- 

-     session.removeHostFromChannel(host, channel)

- 

- def handle_remove_channel(options, session, args):

-     "[admin] Remove a channel entirely"

-     usage = _("usage: %prog remove-channel [options] channel")

-     usage += _("\n(Specify the --help global option for a list of other help options)")

-     parser = OptionParser(usage=usage)

-     parser.add_option("--force", action="store_true", help=_("force removal, if possible"))

-     (options, args) = parser.parse_args(args)

-     if len(args) != 1:

-         parser.error(_("Incorrect number of arguments"))

-         assert False  # pragma: no cover

-     activate_session(session)

-     cinfo = session.getChannel(args[0])

-     if not cinfo:

-         print("No such channel: %s" % args[0])

-         return 1

-     session.removeChannel(args[0], force=options.force)

- 

- def handle_rename_channel(options, session, args):

-     "[admin] Rename a channel"

-     usage = _("usage: %prog rename-channel [options] old-name new-name")

-     usage += _("\n(Specify the --help global option for a list of other help options)")

-     parser = OptionParser(usage=usage)

-     (options, args) = parser.parse_args(args)

-     if len(args) != 2:

-         parser.error(_("Incorrect number of arguments"))

-         assert False  # pragma: no cover

-     activate_session(session)

-     cinfo = session.getChannel(args[0])

-     if not cinfo:

-         print("No such channel: %s" % args[0])

-         return 1

-     session.renameChannel(args[0], args[1])

+         list_commands()

+         sys.exit(0)

  

- def handle_add_pkg(options, session, args):

-     "[admin] Add a package to the listing for tag"

-     usage = _("usage: %prog add-pkg [options] tag package [package2 ...]")

-     usage += _("\n(Specify the --help global option for a list of other help options)")

-     parser = OptionParser(usage=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"))

-         assert False  # pragma: no cover

-     if not options.owner:

-         parser.error(_("Please specify an owner for the package(s)"))

+     aliases = {

+         'cancel-task' : 'cancel',

+         'cxl' : 'cancel',

+         'list-commands' : 'help',

+         'move-pkg': 'move-build',

+         'move': 'move-build',

+         'latest-pkg': 'latest-build',

+         'tag-pkg': 'tag-build',

+         'tag': 'tag-build',

+         'untag-pkg': 'untag-build',

+         'untag': 'untag-build',

+         'watch-tasks': 'watch-task',

+     }

+     cmd = args[0]

+     cmd = aliases.get(cmd, cmd)

+     if cmd.lower() in greetings:

+         cmd = "moshimoshi"

+     cmd = cmd.replace('-', '_')

+     if ('anon_handle_' + cmd) in globals():

+         if not options.force_auth and '--mine' not in args:

+             options.noauth = True

+         cmd = 'anon_handle_' + cmd

+     elif ('handle_' + cmd) in globals():

+         cmd = 'handle_' + cmd

+     else:

+         list_commands()

+         parser.error('Unknown command: %s' % args[0])

          assert False  # pragma: no cover

-     if not session.getUser(options.owner):

-         print("User %s does not exist" % options.owner)

-         return 1

-     activate_session(session)

-     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:

-         print("No such tag: %s" % tag)

-         sys.exit(1)

-     pkglist = dict([(p['package_name'], p['package_id']) for p in session.listPackages(tagID=dsttag['id'])])

-     to_add = []

-     for package in args[1:]:

-         package_id = pkglist.get(package, None)

-         if not package_id is None:

-             print("Package %s already exists in tag %s" % (package, tag))

-             continue

-         to_add.append(package)

-     if options.extra_arches:

-         opts['extra_arches'] = 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)

+     return options, cmd, args[1:]

  

  

- def handle_block_pkg(options, session, args):

-     "[admin] Block a package in the listing for tag"

-     usage = _("usage: %prog block-pkg [options] tag package [package2 ...]")

+ def handle_help(options, session, args):

+     "[info] List available commands"

+     usage = _("usage: %prog help <category> ...")

      usage += _("\n(Specify the --help global option for a list of other help options)")

      parser = OptionParser(usage=usage)

-     (options, args) = parser.parse_args(args)

-     if len(args) < 2:

-         parser.error(_("Please specify a tag and at least one package"))

-         assert False  # pragma: no cover

-     activate_session(session)

-     tag = args[0]

-     # check if list of packages exists for that tag already

-     dsttag=session.getTag(tag)

-     if dsttag is None:

-         print("No such tag: %s" % tag)

-         return 1

-     pkglist = dict([(p['package_name'], p['package_id']) for p in session.listPackages(tagID=dsttag['id'], inherited=True)])

-     ret = 0

-     for package in args[1:]:

-         package_id = pkglist.get(package, None)

-         if package_id is None:

-             print("Package %s doesn't exist in tag %s" % (package, tag))

-             ret = 1

-     if ret:

-         return ret

-     session.multicall = True

-     for package in args[1:]:

-         session.packageListBlock(tag, package)

-     session.multiCall(strict=True)

+     # the --admin opt is for backwards compatibility. It is equivalent to: koji help admin

+     parser.add_option("--admin", action="store_true", help=optparse.SUPPRESS_HELP)

  

- def handle_remove_pkg(options, session, args):

-     "[admin] Remove a package from the listing for tag"

-     usage = _("usage: %prog remove-pkg [options] tag package [package2 ...]")

-     usage += _("\n(Specify the --help global option for a list of other help options)")

-     parser = OptionParser(usage=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"))

-         assert False  # pragma: no cover

-     activate_session(session)

-     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:

-         print("No such tag: %s" % tag)

-         return 1

-     pkglist = dict([(p['package_name'], p['package_id']) for p in session.listPackages(tagID=dsttag['id'])])

-     ret = 0

-     for package in args[1:]:

-         package_id = pkglist.get(package, None)

-         if package_id is None:

-             print("Package %s is not in tag %s" % (package, tag))

-             ret = 1

-     if ret:

-         return ret

-     session.multicall = True

-     for package in args[1:]:

-         session.packageListRemove(tag, package, **opts)

-     session.multiCall(strict=True)

  

- def _unique_path(prefix):

-     """Create a unique path fragment by appending a path component

-     to prefix.  The path component will consist of a string of letter and numbers

-     that is unlikely to be a duplicate, but is not guaranteed to be unique."""

-     # Use time() in the dirname to provide a little more information when

-     # browsing the filesystem.

-     # For some reason repr(time.time()) includes 4 or 5

-     # more digits of precision than str(time.time())

-     return '%s/%r.%s' % (prefix, time.time(),

-                       ''.join([random.choice(string.ascii_letters) for i in range(8)]))

+     chosen = set(args)

+     if options.admin:

+         chosen.add('admin')

+     avail = set(list(categories.keys()) + ['all'])

+     unavail = chosen - avail

+     for arg in unavail:

+         print("No such help category: %s" % arg)

  

- def _format_size(size):

-     if (size / 1073741824 >= 1):

-         return "%0.2f GiB" % (size / 1073741824.0)

-     if (size / 1048576 >= 1):

-         return "%0.2f MiB" % (size / 1048576.0)

-     if (size / 1024 >=1):

-         return "%0.2f KiB" % (size / 1024.0)

-     return "%0.2f B" % (size)

+     if not chosen:

+         list_commands()

+     else:

+         list_commands(chosen)

  

- def _format_secs(t):

-     h = t / 3600

-     t %= 3600

-     m = t / 60

-     s = t % 60

-     return "%02d:%02d:%02d" % (h, m, s)

  

- def _progress_callback(uploaded, total, piece, time, total_time):

-     if total == 0:

-         percent_done = 0.0

+ def list_commands(categories_chosen=None):

+     if categories_chosen is None or "all" in categories_chosen:

+         categories_chosen = list(categories.keys())

      else:

-         percent_done = float(uploaded)/float(total)

-     percent_done_str = "%02d%%" % (percent_done * 100)

-     data_done = _format_size(uploaded)

-     elapsed = _format_secs(total_time)

+         # copy list since we're about to modify it

+         categories_chosen = list(categories_chosen)

+     categories_chosen.sort()

+     handlers = []

+     for name,value in globals().items():

+         if name.startswith('handle_'):

+             alias = name.replace('handle_','')

+             alias = alias.replace('_','-')

+             handlers.append((alias,value))

+         elif name.startswith('anon_handle_'):

+             alias = name.replace('anon_handle_','')

+             alias = alias.replace('_','-')

+             handlers.append((alias,value))

+     handlers.sort()

+     print(_("Available commands:"))

+     for category in categories_chosen:

+         print(_("\n%s:" % categories[category]))

+         for alias,handler in handlers:

+             desc = handler.__doc__

+             if desc.startswith('[%s] ' % category):

+                 desc = desc[len('[%s] ' % category):]

+             elif category != 'misc' or desc.startswith('['):

+                 continue

+             print("        %-25s %s" % (alias, desc))

  

-     speed = "- B/sec"

-     if (time):

-         if (uploaded != total):

-             speed = _format_size(float(piece)/float(time)) + "/sec"

-         else:

-             speed = _format_size(float(total)/float(total_time)) + "/sec"

+     print("%s" % get_epilog_str().rstrip("\n"))

  

-     # write formated string and flush

-     sys.stdout.write("[% -36s] % 4s % 8s % 10s % 14s\r" % ('='*(int(percent_done*36)), percent_done_str, elapsed, data_done, speed))

-     sys.stdout.flush()

- 

- def _running_in_bg():

-     try:

-         return (not os.isatty(0)) or (os.getpgrp() != os.tcgetpgrp(0))

-     except OSError as e:

-         return True

- 

- def handle_build(options, session, args):

-     "[build] Build a package from source"

-     usage = _("usage: %prog build [options] target <srpm path or scm url>")

-     usage += _("\n(Specify the --help global option for a list of other help options)")

-     parser = OptionParser(usage=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("--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)

-     parser.add_option("--arch-override", help=_("Override build arches"))

-     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"))

-     (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"))

-         assert False  # pragma: no cover

-     if build_opts.arch_override and not build_opts.scratch:

-         parser.error(_("--arch_override is only allowed for --scratch builds"))

-     activate_session(session)

-     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(_("Unknown build target: %s" % target))

-         dest_tag = session.getTag(build_target['dest_tag'])

-         if not dest_tag:

-             parser.error(_("Unknown 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'] = parse_arches(build_opts.arch_override)

-     for key in ('skip_tag', 'scratch', 'repo_id'):

-         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

-     # 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)

-     else:

-         return

- 

- 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] target URL [URL2 [:] URL3 [:] URL4 ...]")

-     usage += _("\n(Specify the --help global option for a list of other help options)")

-     parser = OptionParser(usage=usage)

-     parser.add_option("--nowait", action="store_true",

-                       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"))

-         assert False  # pragma: no cover

-     activate_session(session)

-     target = args[0]

-     build_target = session.getBuildTarget(target)

-     if not build_target:

-         parser.error(_("Unknown 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]:

-         print(_("Packages in destination tag %(dest_tag_name)s are not inherited by build tag %(build_tag_name)s" % build_target))

-         print(_("Target %s is not usable for a chain-build" % build_target['name']))

-         return 1

- 

-     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:

-             print(_('"%s" is not a SCM URL or package N-V-R' % src))

-             return 1

-     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)\nIf 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 _running_in_bg() or build_opts.nowait:

-         return

-     else:

-         session.logout()

-         return watch_tasks(session, [task_id], quiet=build_opts.quiet)

- 

- def handle_maven_build(options, session, args):

-     "[build] Build a Maven package from source"

-     usage = _("usage: %prog maven-build [options] target URL")

-     usage += _("\n       %prog maven-build --ini=CONFIG... [options] target")

-     usage += _("\n(Specify the --help global option for a list of other help options)")

-     parser = OptionParser(usage=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("--nowait", action="store_true",

-                       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)

-     target = args[0]

-     build_target = session.getBuildTarget(target)

-     if not build_target:

-         parser.error(_("Unknown build target: %s" % target))

-     dest_tag = session.getTag(build_target['dest_tag'])

-     if not dest_tag:

-         parser.error(_("Unknown 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 = list(params.values())[0]

-         if opts.pop('type', 'maven') != 'maven':

-             parser.error(_("Section %s does not contain a maven-build config") % 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(_("Invalid 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 _running_in_bg() or build_opts.nowait:

-         return

-     else:

-         session.logout()

-         return watch_tasks(session, [task_id], quiet=build_opts.quiet)

- 

- def handle_wrapper_rpm(options, session, args):

-     """[build] Build wrapper rpms for any archives associated with a build."""

-     usage = _("usage: %prog wrapper-rpm [options] target build-id|n-v-r URL")

-     usage += _("\n(Specify the --help global option for a list of other help options)")

-     parser = OptionParser(usage=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("--nowait", action="store_true", 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)

- 

-     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 = list(params.values())[0]

-         if opts.get('type') != 'wrapper':

-             parser.error(_("Section %s does not contain a wrapper-rpm config") % 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 _running_in_bg() or build_opts.nowait:

-         return

-     else:

-         session.logout()

-         return watch_tasks(session,[task_id],quiet=options.quiet)

- 

- def handle_maven_chain(options, session, args):

-     "[build] Run a set of Maven builds in dependency order"

-     usage = _("usage: %prog maven-chain [options] target config...")

-     usage += _("\n(Specify the --help global option for a list of other help options)")

-     parser = OptionParser(usage=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("--nowait", action="store_true",

-                       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"))

-         assert False  # pragma: no cover

-     activate_session(session)

-     target = args[0]

-     build_target = session.getBuildTarget(target)

-     if not build_target:

-         parser.error(_("Unknown build target: %s") % target)

-     dest_tag = session.getTag(build_target['dest_tag'])

-     if not dest_tag:

-         parser.error(_("Unknown 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 _running_in_bg() or build_opts.nowait:

-         return

-     else:

-         session.logout()

-         return watch_tasks(session, [task_id], quiet=options.quiet)

- 

- def handle_resubmit(options, session, args):

-     """[build] Retry a canceled or failed task, using the same parameter as the original task."""

-     usage = _("usage: %prog resubmit [options] taskID")

-     usage += _("\n(Specify the --help global option for a list of other help options)")

-     parser = OptionParser(usage=usage)

-     parser.add_option("--nowait", action="store_true", 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", help=_("Do not print the task information"), default=options.quiet)

-     (options, args) = parser.parse_args(args)

-     if len(args) != 1:

-         parser.error(_("Please specify a single task ID"))

-         assert False  # pragma: no cover

-     activate_session(session)

-     taskID = int(args[0])

-     if not options.quiet:

-         print("Resubmitting the following task:")

-         _printTaskInfo(session, taskID, 0, False, True)

-     newID = session.resubmitTask(taskID)

-     if not options.quiet:

-         print("Resubmitted task %s as new task %s" % (taskID, newID))

-     if _running_in_bg() or options.nowait:

-         return

-     else:

-         session.logout()

-         return watch_tasks(session, [newID], quiet=options.quiet)

- 

- def handle_call(options, session, args):

-     "Execute an arbitrary XML-RPC call"

-     usage = _("usage: %prog call [options] name [arg...]")

-     usage += _("\n(Specify the --help global option for a list of other help options)")

-     parser = OptionParser(usage=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"))

-         assert False  # pragma: no cover

-     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)

-     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=(',', ': ')))

-     else:

-         pprint.pprint(response)

- 

- def anon_handle_mock_config(options, session, args):

-     "[info] Create a mock config"

-     usage = _("usage: %prog mock-config [options]")

-     usage += _("\n(Specify the --help global option for a list of other help options)")

-     parser = OptionParser(usage=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"))

-     parser.add_option("--topurl", metavar="URL", default=options.topurl,

-                       help=_("URL under which Koji files are accessible"))

-     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)

-     activate_session(session)

-     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):

-             opts[k] = getattr(options, k)

-     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 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:

-             print(_("No buildroots for task %s (or no such task)") % options.task)

-             return 1

-         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']

-         def_name = "%s-task_%i" % (opts['tag_name'], task_id)

-     elif options.tag:

-         if not options.arch:

-             print(_("Please specify an arch"))

-             return 1

-         tag = session.getTag(options.tag)

-         if not tag:

-             parser.error(_("Invalid tag: %s" % options.tag))

-         arch = options.arch

-         config = session.getBuildConfig(tag['id'])

-         if not config:

-             print(_("Could not get config info for tag: %(name)s") % tag)

-             return 1

-         opts['tag_name'] = tag['name']

-         if options.latest:

-             opts['repoid'] = 'latest'

-         else:

-             repo = session.getRepo(config['id'])

-             if not repo:

-                 print(_("Could not get a repo for tag: %(name)s") % tag)

-                 return 1

-             opts['repoid'] = repo['id']

-         def_name = "%(tag_name)s-repo_%(repoid)s" % opts

-     elif options.target:

-         if not options.arch:

-             print(_("Please specify an arch"))

-             return 1

-         arch = options.arch

-         target = session.getBuildTarget(options.target)

-         if not target:

-             parser.error(_("Invalid 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:

-                 print(_("Could not get a repo for tag: %(name)s") % opts['tag_name'])

-                 return 1

-             opts['repoid'] = repo['id']

-     else:

-         parser.error(_("Please specify one of: --tag, --target, --task, --buildroot"))

-         assert False  # pragma: no cover

-     if options.name:

-         name = options.name

-     else:

-         name = "%(tag_name)s-repo_%(repoid)s" % opts

-     output = koji.genMockConfig(name, arch, **opts)

-     if options.ofile:

-         fo = open(options.ofile, 'w')

-         fo.write(output)

-         fo.close()

-     else:

-         print(output)

- 

- def handle_disable_host(options, session, args):

-     "[admin] Mark one or more hosts as disabled"

-     usage = _("usage: %prog disable-host [options] hostname ...")

-     usage += _("\n(Specify the --help global option for a list of other help options)")

-     parser = OptionParser(usage=usage)

-     parser.add_option("--comment", help=_("Comment indicating why the host(s) are being disabled"))

-     (options, args) = parser.parse_args(args)

- 

-     activate_session(session)

-     session.multicall = True

-     for host in args:

-         session.getHost(host)

-     error = False

-     for host, [id] in zip(args, session.multiCall(strict=True)):

-         if not id:

-             print("Host %s does not exist" % host)

-             error = True

-     if error:

-         print("No changes made. Please correct the command line.")

-         return 1

-     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(options, session, args):

-     "[admin] Mark one or more hosts as enabled"

-     usage = _("usage: %prog enable-host [options] hostname ...")

-     usage += _("\n(Specify the --help global option for a list of other help options)")

-     parser = OptionParser(usage=usage)

-     parser.add_option("--comment", help=_("Comment indicating why the host(s) are being enabled"))

-     (options, args) = parser.parse_args(args)

- 

-     activate_session(session)

-     session.multicall = True

-     for host in args:

-         session.getHost(host)

-     error = False

-     for host, [id] in zip(args, session.multiCall(strict=True)):

-         if not id:

-             print("Host %s does not exist" % host)

-             error = True

-     if error:

-         print("No changes made. Please correct the command line.")

-         return 1

-     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]")

-     usage += _("\n(Specify the --help global option for a list of other help options)")

-     parser = OptionParser(usage=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)

-     (my_opts, args) = parser.parse_args(args)

- 

-     activate_session(session)

-     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)

-     else:

-         return

- 

- 

- def linked_upload(localfile, path, name=None):

-     """Link a file into the (locally writable) workdir, bypassing upload"""

-     old_umask = os.umask(0o02)

-     try:

-         if name is None:

-             name = os.path.basename(localfile)

-         dest_dir = os.path.join(koji.pathinfo.work(), path)

-         dst = os.path.join(dest_dir, name)

-         koji.ensuredir(dest_dir)

-         # fix uid/gid to keep httpd happy

-         st = os.stat(koji.pathinfo.work())

-         os.chown(dest_dir, st.st_uid, st.st_gid)

-         print("Linking rpm to: %s" % dst)

-         os.link(localfile, dst)

-     finally:

-         os.umask(old_umask)

- 

- 

- def handle_import(options, session, args):

-     "[admin] Import externally built RPMs into the database"

-     usage = _("usage: %prog import [options] package [package...]")

-     usage += _("\n(Specify the --help global option for a list of other help options)")

-     parser = OptionParser(usage=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"))

-         assert False  # pragma: no cover

-     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)

-             assert False  # pragma: no cover

-     activate_session(session)

-     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 = list(to_import.keys())

-     nvrs.sort()

-     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:

-                 print(_("WARNING: md5sum mismatch for %s") % path)

-                 print(_("  A different rpm with the same name has already been imported"))

-                 print(_("  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:

-                 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(options, session, args):

-     "[admin] Import external builds with rich metadata"

-     usage = _("usage: %prog import-cg [options] metadata_file files_dir")

-     usage += _("\n(Specify the --help global option for a list of other help options)")

-     parser = OptionParser(usage=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"))

-     (options, args) = parser.parse_args(args)

-     if len(args) < 2:

-         parser.error(_("Please specify metadata files directory"))

-         assert False  # pragma: no cover

-     if json is None:

-         parser.error(_("Unable to find json module"))

-         assert False  # pragma: no cover

-     activate_session(session)

-     metadata = json.load(open(args[0], 'r'))

-     if 'output' not in metadata:

-         print(_("Metadata contains no output"))

-         sys.exit(1)

-     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)

- 

- 

- def handle_import_comps(options, session, args):

-     "Import group/package information from a comps file"

-     usage = _("usage: %prog import-comps [options] <file> <tag>")

-     usage += _("\n(Specify the --help global option for a list of other help options)")

-     parser = OptionParser(usage=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"))

-         assert False  # pragma: no cover

-     activate_session(session)

-     # check if the tag exists

-     dsttag = session.getTag(args[1])

-     if dsttag is None:

-         print("No such tag: %s" % args[1])

-         return 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:

-         print("comps module not available")

-         return 1

- 

- 

- 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):

-                     pkgopts[k] = str(pkgopts[k])

-             s_opts = ', '.join(["'%s': %r" % (k, pkgopts[k]) for k in sorted(list(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):

-     """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):

-                         pkgopts[k] = str(pkgopts[k])

-                 s_opts = ', '.join(["'%s': %r" % (k, pkgopts[k]) for k in sorted(list(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(options, session, args):

-     "[admin] Import signatures into the database"

-     usage = _("usage: %prog import-sig [options] package [package...]")

-     usage += _("\n(Specify the --help global option for a list of other help options)")

-     parser = OptionParser(usage=usage)

-     parser.add_option("--with-unsigned", action="store_true",

-                       help=_("Also import unsigned sig headers"))

-     parser.add_option("--write", action="store_true",

-                       help=_("Also write the signed copies"))

-     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"))

-         assert False  # pragma: no cover

-     for path in args:

-         if not os.path.exists(path):

-             parser.error(_("No such file: %s") % path)

-     activate_session(session)

-     for path in args:

-         data = koji.get_header_fields(path, ('name','version','release','arch','siggpg','sigpgp','sourcepackage'))

-         if data['sourcepackage']:

-             data['arch'] = 'src'

-         sigkey = data['siggpg']

-         if not sigkey:

-             sigkey = data['sigpgp']

-         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']

-         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:

-                 print(_("Warning: signature mismatch: %s") % path)

-                 print(_("  The system already has a signature for this rpm with key %s") % sigkey)

-                 print(_("  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'], base64.encodestring(sighdr))

-         print(_("Writing signed copy"))

-         if not options.test:

-             session.writeSignedRPM(rinfo['id'], sigkey)

- 

- 

- def handle_write_signed_rpm(options, session, args):

-     "[admin] Write signed RPMs to disk"

-     usage = _("usage: %prog write-signed-rpm [options] <signature-key> n-v-r [n-v-r...]")

-     usage += _("\n(Specify the --help global option for a list of other help options)")

-     parser = OptionParser(usage=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"))

-         assert False  # pragma: no cover

-     if len(args) < 2 and not (options.all or options.buildid):

-         parser.error(_("At least one RPM must be specified"))

-         assert False  # pragma: no cover

-     key = args.pop(0)

-     activate_session(session)

-     if options.all:

-         rpms = session.queryRPMSigs(sigkey=key)

-         rpms = [session.getRPM(r['rpm_id']) for r in rpms]

-     elif options.buildid:

-         rpms = session.listRPMs(int(options.buildid))

-     else:

-         rpms = []

-         bad = []

-         for nvra in args:

-             try:

-                 koji.parse_NVRA(nvra)

-                 rinfo = session.getRPM(nvra, strict=True)

-                 if rinfo:

-                     rpms.append(rinfo)

-             except koji.GenericError:

-                 bad.append(nvra)

-         # for historical reasons, we also accept nvrs

-         for nvr in bad:

-             build = session.getBuild(nvr)

-             if not build:

-                 raise koji.GenericError("No such rpm or build: %s" % nvr)

-             rpms.extend(session.listRPMs(buildID=build['id']))

-     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))

-         session.writeSignedRPM(rpminfo['id'], key)

- 

- 

- def handle_prune_signed_copies(options, session, args):

-     "[admin] Prune signed copies"

-     usage = _("usage: %prog prune-sigs [options]")

-     usage += _("\n(Specify the --help global option for a list of other help options)")

-     parser = OptionParser(usage=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"))

-     parser.add_option("--debug", action="store_true", help=_("Show debugging output"))

-     (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:

-         fo = open(options.ignore_tag_file)

-         options.ignore_tag.extend([line.strip() for line in fo.readlines()])

-         fo.close()

-     if options.protect_tag_file:

-         fo = open(options.protect_tag_file)

-         options.protect_tag.extend([line.strip() for line in fo.readlines()])

-         fo.close()

-     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)))

-     if not options.build:

-         if options.verbose:

-             print("Getting builds...")

-         qopts = {'state' : koji.BUILD_STATES['COMPLETE']}

-         if options.package:

-             pkginfo = session.getPackage(options.package)

-             qopts['packageID'] = pkginfo['id']

-         builds = [(b['nvr'], b) for b in session.listBuilds(**qopts)]

-         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)

-             assert False  # pragma: no cover

-         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("unknown 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.tagHistory(build=binfo['id']):

-             #we used tagHistory 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" % 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.tagHistory(tag=tag_name, package=binfo['name'])

-             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()

-             #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 = 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:

-                 print("Warning: 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(options, session, args):

-     "[admin] Move a build to a different volume"

-     usage = _("usage: %prog set-build-volume volume n-v-r [n-v-r ...]")

-     usage += _("\n(Specify the --help global option for a list of other help options)")

-     parser = OptionParser(usage=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:

-         print("No such volume: %s" % args[0])

-         return 1

-     activate_session(session)

-     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:

-         print("No builds to move")

-         return 1

-     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(options, session, args):

-     "[admin] Add a new storage volume"

-     usage = _("usage: %prog add-volume volume-name")

-     usage += _("\n(Specify the --help global option for a list of other help options)")

-     parser = OptionParser(usage=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:

-         print("Volume %s already exists" % name)

-         return 1

-     activate_session(session)

-     volinfo = session.addVolume(name)

-     print("Added volume %(name)s with id %(id)i" % volinfo)

- 

- def handle_list_volumes(options, session, args):

-     "[info] List storage volumes"

-     usage = _("usage: %prog list-volumes")

-     usage += _("\n(Specify the --help global option for a list of other help options)")

-     parser = OptionParser(usage=usage)

-     (options, args) = parser.parse_args(args)

-     for volinfo in session.listVolumes():

-         print(volinfo['name'])

- 

- def handle_list_permissions(options, session, args):

-     "[info] List user permissions"

-     usage = _("usage: %prog list-permissions [options]")

-     usage += _("\n(Specify the --help global option for a list of other help options)")

-     parser = OptionParser(usage=usage)

-     parser.add_option("--user", help=_("List permissions for the given user"))

-     parser.add_option("--mine", action="store_true", help=_("List your permissions"))

-     (options, args) = parser.parse_args(args)

-     if len(args) > 0:

-         parser.error(_("This command takes no arguments"))

-         assert False  # pragma: no cover

-     activate_session(session)

-     if options.user:

-         user = session.getUser(options.user)

-         if not user:

-             print("User %s does not exist" % options.user)

-             return 1

-         perms = session.getUserPerms(user['id'])

-     elif options.mine:

-         perms = session.getPerms()

-     else:

-         perms = [p['name'] for p in session.getAllPerms()]

-     for perm in perms:

-         print(perm)

- 

- def handle_add_user(options, session, args):

-     "[admin] Add a user"

-     usage = _("usage: %prog add-user username [options]")

-     usage += _("\n(Specify the --help global option for a list of other help options)")

-     parser = OptionParser(usage=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)

-     user_id = session.createUser(username, status=status, krb_principal=options.principal)

-     print("Added user %s (%i)" % (username, user_id))

- 

- def handle_enable_user(options, session, args):

-     "[admin] Enable logins by a user"

-     usage = _("usage: %prog enable-user username")

-     usage += _("\n(Specify the --help global option for a list of other help options)")

-     parser = OptionParser(usage=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)

-     session.enableUser(username)

- 

- def handle_disable_user(options, session, args):

-     "[admin] Disable logins by a user"

-     usage = _("usage: %prog disable-user username")

-     usage += _("\n(Specify the --help global option for a list of other help options)")

-     parser = OptionParser(usage=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)

-     session.disableUser(username)

- 

- def handle_list_signed(options, session, args):

-     "[admin] List signed copies of rpms"

-     usage = _("usage: %prog list-signed [options]")

-     usage += _("\n(Specify the --help global option for a list of other help options)")

-     parser = OptionParser(usage=usage)

-     parser.add_option("--debug", action="store_true")

-     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)

-     activate_session(session)

-     qopts = {}

-     build_idx = {}

-     rpm_idx = {}

-     if options.key:

-         qopts['sigkey'] = options.key

-     if options.rpm:

-         rinfo = session.getRPM(options.rpm)

-         rpm_idx[rinfo['id']] = rinfo

-         if rinfo is None:

-             parser.error(_("No such RPM: %s") % options.rpm)

-         if rinfo.get('external_repo_id'):

-             print("External rpm: %(name)s-%(version)s-%(release)s.%(arch)s@%(external_repo_name)s" % rinfo)

-             return 1

-         qopts['rpm_id'] = rinfo['id']

-     if options.build:

-         binfo = session.getBuild(options.build)

-         build_idx[binfo['id']] = binfo

-         if binfo is None:

-             parser.error(_("No such build: %s") % options.rpm)

-         sigs = []

-         rpms = session.listRPMs(buildID=binfo['id'])

-         for rinfo in rpms:

-             rpm_idx[rinfo['id']] = rinfo

-             sigs += session.queryRPMSigs(rpm_id=rinfo['id'], **qopts)

-     else:

-         sigs = session.queryRPMSigs(**qopts)

-     if options.tag:

-         print("getting tag listing")

-         rpms, builds = session.listTaggedRPMS(options.tag, inherit=False, latest=False)

-         print("got tag listing")

-         tagged = {}

-         for binfo in builds:

-             build_idx.setdefault(binfo['id'], binfo)

-         for rinfo in rpms:

-             rpm_idx.setdefault(rinfo['id'], rinfo)

-             tagged[rinfo['id']] = 1

-     #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 options.debug:

-                 print("No copy: %s" % signedpath)

-             continue

-         print(signedpath)

- 

- def handle_import_in_place(options, session, args):

-     "[admin] Import RPMs that are already in place"

-     usage = _("usage: %prog import-in-place [options] package [package...]")

-     usage += _("\n(Specify the --help global option for a list of other help options)")

-     parser = OptionParser(usage=usage)

-     (options, args) = parser.parse_args(args)

-     if len(args) < 1:

-         parser.error(_("At least one package must be specified"))

-         assert False  # pragma: no cover

-     activate_session(session)

-     for nvr in args:

-         data = koji.parse_NVR(nvr)

-         sys.stdout.write(_("importing %s... ") % nvr)

-         try:

-             session.importBuildInPlace(data)

-         except koji.GenericError as e:

-             print(_("\nError importing: %s" % str(e).splitlines()[-1]))

-             sys.stdout.flush()

-         else:

-             print(_("done"))

-         sys.stdout.flush()

- 

- def handle_import_archive(options, session, args):

-     "[admin] Import an archive file and associate it with a build"

-     usage = _("usage: %prog import-archive build-id|n-v-r /path/to/archive...")

-     usage += _("\n(Specify the --help global option for a list of other help options)")

-     parser = OptionParser(usage=usage)

-     parser.add_option("--noprogress", action="store_true",

-                       help=_("Do not display progress of the upload"))

-     parser.add_option("--create-build", action="store_true", help=_("Auto-create builds as needed"))

-     parser.add_option("--link", action="store_true", help=_("Attempt to hardlink instead of uploading"))

-     parser.add_option("--type", help=_("The type of archive being imported.  Currently supported types: maven, win, image"))

-     parser.add_option("--type-info", help=_("Type-specific information to associate with the archives.  "

-                                             "For Maven archives this should be a local path to a .pom file.  "

-                                             "For Windows archives this should be relpath:platforms[:flags]))  "

-                                             "Images need an arch"))

-     (suboptions, args) = parser.parse_args(args)

- 

-     if not len(args) > 1:

-         parser.error(_("You must specify a build ID or N-V-R and an archive to import"))

-         assert False  # pragma: no cover

- 

-     activate_session(session)

- 

-     if not suboptions.type:

-         parser.error(_("You must specify an archive type"))

-         assert False  # pragma: no cover

-     if suboptions.type == 'maven':

-         if not (session.hasPerm('maven-import') or session.hasPerm('admin')):

-             parser.error(_("This action requires the maven-import privilege"))

-             assert False  # pragma: no cover

-         if not suboptions.type_info:

-             parser.error(_("--type-info must point to a .pom file when importing Maven archives"))

-             assert False  # pragma: no cover

-         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"))

-             assert False  # pragma: no cover

-         if not suboptions.type_info:

-             parser.error(_("--type-info must be specified"))

-             assert False  # pragma: no cover

-         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"))

-             assert False  # pragma: no cover

-         if not suboptions.type_info:

-             parser.error(_("--type-info must be specified"))

-             assert False  # pragma: no cover

-         image_info = {'arch': suboptions.type_info}

-         suboptions.type_info = image_info

-     else:

-         parser.error(_("Unsupported archive type: %s" % suboptions.type))

-         assert False  # pragma: no cover

- 

-     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(options, session, args):

-     "[admin] Grant a permission to a user"

-     usage = _("usage: %prog grant-permission <permission> <user> [<user> ...]")

-     usage += _("\n(Specify the --help global option for a list of other help options)")

-     parser = OptionParser(usage=usage)

-     parser.add_option("--new", action="store_true", help=_("Create a new permission"))

-     (options, args) = parser.parse_args(args)

-     if len(args) < 2:

-         parser.error(_("Please specify a permission and at least one user"))

-         assert False  # pragma: no cover

-     activate_session(session)

-     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))

-             assert False  # pragma: no cover

-         users.append(user)

-     kwargs = {}

-     if options.new:

-         kwargs['create'] = True

-     for user in users:

-         session.grantPermission(user['name'], perm, **kwargs)

- 

- def handle_revoke_permission(options, session, args):

-     "[admin] Revoke a permission from a user"

-     usage = _("usage: %prog revoke-permission <permission> <user> [<user> ...]")

-     usage += _("\n(Specify the --help global option for a list of other help options)")

-     parser = OptionParser(usage=usage)

-     (options, args) = parser.parse_args(args)

-     if len(args) < 2:

-         parser.error(_("Please specify a permission and at least one user"))

-         assert False  # pragma: no cover

-     activate_session(session)

-     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))

-             assert False  # pragma: no cover

-         users.append(user)

-     for user in users:

-         session.revokePermission(user['name'], perm)

- 

- 

- def handle_grant_cg_access(options, session, args):

-     "[admin] Add a user to a content generator"

-     usage = _("usage: %prog grant-cg-access <user> <content generator>")

-     usage += _("\n(Specify the --help global option for a list of other help options)")

-     parser = OptionParser(usage=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"))

-         assert False  # pragma: no cover

-     activate_session(session)

-     user = args[0]

-     cg = args[1]

-     uinfo = session.getUser(user)

-     if uinfo is None:

-         parser.error(_("No such user: %s" % user))

-         assert False  # pragma: no cover

-     kwargs = {}

-     if options.new:

-         kwargs['create'] = True

-     session.grantCGAccess(uinfo['name'], cg, **kwargs)

- 

- 

- def handle_revoke_cg_access(options, session, args):

-     "[admin] Remove a user from a content generator"

-     usage = _("usage: %prog revoke-cg-access <user> <content generator>")

-     usage += _("\n(Specify the --help global option for a list of other help options)")

-     parser = OptionParser(usage=usage)

-     (options, args) = parser.parse_args(args)

-     if len(args) != 2:

-         parser.error(_("Please specify a user and content generator"))

-         assert False  # pragma: no cover

-     activate_session(session)

-     user = args[0]

-     cg = args[1]

-     uinfo = session.getUser(user)

-     if uinfo is None:

-         parser.error(_("No such user: %s" % user))

-         assert False  # pragma: no cover

-     session.revokeCGAccess(uinfo['name'], cg)

- 

- 

- def anon_handle_latest_build(options, session, args):

-     "[info] Print the latest builds for a tag"

-     usage = _("usage: %prog latest-build [options] tag package [package...]")

-     usage += _("\n(Specify the --help global option for a list of other help options)")

-     parser = OptionParser(usage=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", help=_("Do not print the header information"), default=options.quiet)

-     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"))

-     (options, args) = parser.parse_args(args)

-     if len(args) == 0:

-         parser.error(_("A tag name must be specified"))

-         assert False  # pragma: no cover

-     activate_session(session)

-     if options.all:

-         if len(args) > 1:

-             parser.error(_("A package name may not be combined with --all"))

-             assert False  # pragma: no cover

-         # 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"))

-             assert False  # pragma: no cover

-     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 = [ fmt % x for x in data]

-         output.sort()

-         for line in output:

-             print(line)

- 

- 

- def anon_handle_list_api(options, session, args):

-     "[info] Print the list of XML-RPC APIs"

-     usage = _("usage: %prog list-api [options]")

-     usage += _("\n(Specify the --help global option for a list of other help options)")

-     parser = OptionParser(usage=usage)

-     (options, args) = parser.parse_args(args)

-     if len(args) != 0:

-         parser.error(_("This command takes no arguments"))

-         assert False  # pragma: no cover

-     activate_session(session)

-     tmplist = [(x['name'], x) for x in session._listapi()]

-     tmplist.sort()

-     funcs = [x[1] for x in tmplist]

-     for x in funcs:

-         if 'argdesc' in x:

-             args = x['argdesc']

-         elif x['args']:

-             # older servers may not provide argdesc

-             expanded = []

-             for arg in x['args']:

-                 if type(arg) is 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(options, session, args):

-     "[info] List the builds or rpms in a tag"

-     usage = _("usage: %prog list-tagged [options] tag [package]")

-     usage += _("\n(Specify the --help global option for a list of other help options)")

-     parser = OptionParser(usage=usage)

-     parser.add_option("--arch", 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", help=_("Do not print the header information"), default=options.quiet)

-     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 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"))

-         assert False  # pragma: no cover

-     elif len(args) > 2:

-         parser.error(_("Only one package name may be specified"))

-         assert False  # pragma: no cover

-     activate_session(session)

-     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)

-     if event:

-         opts['event'] = event['id']

-         event['timestr'] = time.asctime(time.localtime(event['ts']))

-         if not options.quiet:

-             print("Querying at event %(id)i (%(timestr)s)" % event)

- 

-     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 = [ fmt % x for x in data]

-     output.sort()

-     for line in output:

-         print(line)

- 

- def anon_handle_list_buildroot(options, session, args):

-     "[info] List the rpms used in or built in a buildroot"

-     usage = _("usage: %prog list-buildroot [options] buildroot-id")

-     usage += _("\n(Specify the --help global option for a list of other help options)")

-     parser = OptionParser(usage=usage)

-     parser.add_option("--paths", action="store_true", help=_("Show the file paths"))

-     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"))

-         assert False  # pragma: no cover

-     activate_session(session)

-     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 = [(fmt % x, x) for x in data]

-     order.sort()

-     for nvra, rinfo in order:

-         if options.verbose and rinfo.get('is_update'):

-             print(nvra, "[update]")

-         else:

-             print(nvra)

- 

- def anon_handle_list_untagged(options, session, args):

-     "[info] List untagged builds"

-     usage = _("usage: %prog list-untagged [options] [package]")

-     usage += _("\n(Specify the --help global option for a list of other help options)")

-     parser = OptionParser(usage=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"))

-         assert False  # pragma: no cover

-     activate_session(session)

-     package = None

-     if len(args) > 0:

-         package = args[0]

-     opts = {}

-     if package:

-         opts['name'] = package

-     pathinfo = koji.PathInfo()

- 

-     data = session.untaggedBuilds(**opts)

-     if options.show_references:

-         print("(Showing build references)")

-         refs = {}

-         refs2 = {} #reverse map

-         for x in session.buildMap():

-             refs.setdefault(x['used'], {}).setdefault(x['built'], 1)

-             refs2.setdefault(x['built'], {}).setdefault(x['used'], 1)

-         #XXX - need to ignore refs to unreferenced builds

-         for x in data:

-             builds = refs.get(x['id'])

-             if builds:

-                 x['refs'] = "%s" % builds

-             else:

-                 x['refs'] = ''

-         #data = [x for x in data if x['id'] not in refs)]

-     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 = [ fmt % x for x in data]

-     output.sort()

-     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):

-     print("  %(package)s: %(basearchonly)s, %(type)s  [%(tag_name)s]" % pkg)

- 

- def anon_handle_list_groups(options, session, args):

-     "[info] Print the group listings"

-     usage = _("usage: %prog list-groups [options] <tag> [group]")

-     usage += _("\n(Specify the --help global option for a list of other help options)")

-     parser = OptionParser(usage=usage)

-     parser.add_option("--event", type='int', metavar="EVENT#", help=_("query at event"))

-     parser.add_option("--ts", type='int', metavar="TIMESTAMP", help=_("query at 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 or len(args) > 2:

-         parser.error(_("Incorrect number of arguments"))

-         assert False  # pragma: no cover

-     opts = {}

-     activate_session(session)

-     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)

-     tags = dict([(x['id'], x['name']) for x in session.listTags()])

-     tmp_list = [(x['name'], x) for x in session.getTagGroups(args[0], **opts)]

-     tmp_list.sort()

-     groups = [x[1] for x in tmp_list]

-     for group in groups:

-         if len(args) > 1 and group['name'] != args[1]:

-             continue

-         print("%s  [%s]" % (group['name'], tags.get(group['tag_id'], group['tag_id'])))

-         groups = [(x['name'], x) for x in group['grouplist']]

-         groups.sort()

-         for x in [x[1] for x in groups]:

-             x['tag_name'] = tags.get(x['tag_id'], x['tag_id'])

-             print_group_list_req_group(x)

-         pkgs = [(x['package'], x) for x in group['packagelist']]

-         pkgs.sort()

-         for x in [x[1] for x in pkgs]:

-             x['tag_name'] = tags.get(x['tag_id'], x['tag_id'])

-             print_group_list_req_package(x)

- 

- def handle_add_group_pkg(options, session, args):

-     "[admin] Add a package to a group's package listing"

-     usage = _("usage: %prog add-group-pkg [options] <tag> <group> <pkg> [<pkg>...]")

-     usage += _("\n(Specify the --help global option for a list of other help options)")

-     parser = OptionParser(usage=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"))

-         assert False  # pragma: no cover

-     tag = args[0]

-     group = args[1]

-     activate_session(session)

-     for pkg in args[2:]:

-         session.groupPackageListAdd(tag, group, pkg)

- 

- def handle_block_group_pkg(options, session, args):

-     "[admin] Block a package from a group's package listing"

-     usage = _("usage: %prog block-group-pkg [options] <tag> <group> <pkg> [<pkg>...]")

-     usage += _("\n(Specify the --help global option for a list of other help options)")

-     parser = OptionParser(usage=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"))

-         assert False  # pragma: no cover

-     tag = args[0]

-     group = args[1]

-     activate_session(session)

-     for pkg in args[2:]:

-         session.groupPackageListBlock(tag, group, pkg)

- 

- def handle_unblock_group_pkg(options, session, args):

-     "[admin] Unblock a package from a group's package listing"

-     usage = _("usage: %prog unblock-group-pkg [options] <tag> <group> <pkg> [<pkg>...]")

-     usage += _("\n(Specify the --help global option for a list of other help options)")

-     parser = OptionParser(usage=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"))

-         assert False  # pragma: no cover

-     tag = args[0]

-     group = args[1]

-     activate_session(session)

-     for pkg in args[2:]:

-         session.groupPackageListUnblock(tag, group, pkg)

- 

- def handle_add_group_req(options, session, args):

-     "[admin] Add a group to a group's required list"

-     usage = _("usage: %prog add-group-req [options] <tag> <target group> <required group>")

-     usage += _("\n(Specify the --help global option for a list of other help options)")

-     parser = OptionParser(usage=usage)

-     (options, args) = parser.parse_args(args)

-     if len(args) != 3:

-         parser.error(_("You must specify a tag name and two group names"))

-         assert False  # pragma: no cover

-     tag = args[0]

-     group = args[1]

-     req = args[2]

-     activate_session(session)

-     session.groupReqListAdd(tag, group, req)

- 

- def handle_block_group_req(options, session, args):

-     "[admin] Block a group's requirement listing"

-     usage = _("usage: %prog block-group-req [options] <tag> <group> <blocked req>")

-     usage += _("\n(Specify the --help global option for a list of other help options)")

-     parser = OptionParser(usage=usage)

-     (options, args) = parser.parse_args(args)

-     if len(args) != 3:

-         parser.error(_("You must specify a tag name and two group names"))

-         assert False  # pragma: no cover

-     tag = args[0]

-     group = args[1]

-     req = args[2]

-     activate_session(session)

-     session.groupReqListBlock(tag, group, req)

- 

- def handle_unblock_group_req(options, session, args):

-     "[admin] Unblock a group's requirement listing"

-     usage = _("usage: %prog unblock-group-req [options] <tag> <group> <requirement>")

-     usage += _("\n(Specify the --help global option for a list of other help options)")

-     parser = OptionParser(usage=usage)

-     (options, args) = parser.parse_args(args)

-     if len(args) != 3:

-         parser.error(_("You must specify a tag name and two group names"))

-         assert False  # pragma: no cover

-     tag = args[0]

-     group = args[1]

-     req = args[2]

-     activate_session(session)

-     session.groupReqListUnblock(tag, group, req)

- 

- def anon_handle_list_channels(options, session, args):

-     "[info] Print channels listing"

-     usage = _("usage: %prog list-channels")

-     usage += _("\n(Specify the --help global option for a list of other help options)")

-     parser = OptionParser(usage=usage)

-     parser.add_option("--quiet", action="store_true", help=_("Do not print header information"), default=options.quiet)

-     (options, args) = parser.parse_args(args)

-     activate_session(session)

-     channels = session.listChannels()

-     session.multicall = True

-     for channel in channels:

-         session.listHosts(channelID=channel['id'])

-     for channel, hosts in zip(channels, session.multiCall()):

-         channel['hosts'] = len(hosts[0])

-     if not options.quiet:

-         print('Channel         Hosts')

-     for channel in channels:

-         print("%(name)-15s %(hosts) 5d" % channel)

- 

- def anon_handle_list_hosts(options, session, args):

-     "[info] Print the host listing"

-     usage = _("usage: %prog list-hosts [options]")

-     usage += _("\n(Specify the --help global option for a list of other help options)")

-     parser = OptionParser(usage=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("--quiet", action="store_true", help=_("Do not print header information"), default=options.quiet)

-     (options, args) = parser.parse_args(args)

-     opts = {}

-     activate_session(session)

-     if options.arch:

-         opts['arches'] = options.arch

-     if options.channel:

-         channel = session.getChannel(options.channel)

-         if not channel:

-             parser.error(_('Unknown channel: %s' % options.channel))

-             assert False  # pragma: no cover

-         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 = [(x['name'], x) for x in session.listHosts(**opts)]

-     tmp_list.sort()

-     hosts = [x[1] for x in tmp_list]

- 

-     def yesno(x):

-         if x: return 'Y'

-         else: return 'N'

- 

-     # pull in the last update using multicall to speed it up a bit

-     session.multicall = True

-     for host in hosts:

-         session.getLastHostUpdate(host['id'])

-     updateList = session.multiCall()

- 

-     for host, [update] in zip(hosts, updateList):

-         if update is None:

-             host['update'] = '-'

-         else:

-             host['update'] = update.split('.')[0]

-         host['enabled'] = yesno(host['enabled'])

-         host['ready'] = yesno(host['ready'])

-         host['arches'] = ','.join(host['arches'].split())

- 

-     if not options.quiet:

-         print("Hostname                     Enb Rdy Load/Cap Arches           Last Update")

-     for host in hosts:

-         print("%(name)-28s %(enabled)-3s %(ready)-3s %(task_load)4.1f/%(capacity)-3.1f %(arches)-16s %(update)s" % host)

- 

- def anon_handle_list_pkgs(options, session, args):

-     "[info] Print the package listing for tag or for owner"

-     usage = _("usage: %prog list-pkgs [options]")

-     usage += _("\n(Specify the --help global option for a list of other help options)")

-     parser = OptionParser(usage=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", help=_("Do not print header information"), default=options.quiet)

-     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 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"))

-         assert False  # pragma: no cover

-     activate_session(session)

-     opts = {}

-     if options.owner:

-         user = session.getUser(options.owner)

-         if user is None:

-             parser.error(_("Invalid user"))

-             assert False  # pragma: no cover

-         opts['userID'] = user['id']

-     if options.tag:

-         tag = session.getTag(options.tag)

-         if tag is None:

-             parser.error(_("Invalid tag"))

-             assert False  # pragma: no cover

-         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)

-     data = session.listPackages(**opts)

-     if not data:

-         print("(no matching packages)")

-         return 1

-     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_rpminfo(options, session, args):

-     "[info] Print basic information about an RPM"

-     usage = _("usage: %prog rpminfo [options] <n-v-r.a> [<n-v-r.a> ...]")

-     usage += _("\n(Specify the --help global option for a list of other help options)")

-     parser = OptionParser(usage=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"))

-         assert False  # pragma: no cover

-     activate_session(session)

-     for rpm in args:

-         info = session.getRPM(rpm)

-         if info is None:

-             print("No such rpm: %s\n" % rpm)

-             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):

-             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)

- 

- 

- def anon_handle_buildinfo(options, session, args):

-     "[info] Print basic information about a build"

-     usage = _("usage: %prog buildinfo [options] <n-v-r> [<n-v-r> ...]")

-     usage += _("\n(Specify the --help global option for a list of other help options)")

-     parser = OptionParser(usage=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"))

-         assert False  # pragma: no cover

-     activate_session(session)

-     for build in args:

-         if build.isdigit():

-             build = int(build)

-         info = session.getBuild(build)

-         if info is None:

-             print("No such build: %s\n" % build)

-             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)

-         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_time']))

-         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)))

-         rpms = session.listRPMs(buildID=info['id'])

-         image_info = session.getImageBuild(info['id'])

-         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']))

-         if rpms:

-             print("RPMs:")

-             for rpm in rpms:

-                 print(os.path.join(koji.pathinfo.build(info), koji.pathinfo.rpm(rpm)))

-         if options.changelog:

-             changelog = session.getChangelogEntries(info['id'])

-             if changelog:

-                 print("Changelog:")

-                 print(koji.util.formatChangelog(changelog))

- 

- def anon_handle_hostinfo(options, session, args):

-     "[info] Print basic information about a host"

-     usage = _("usage: %prog hostinfo [options] <hostname> [<hostname> ...]")

-     usage += _("\n(Specify the --help global option for a list of other help options)")

-     parser = OptionParser(usage=usage)

-     (options, args) = parser.parse_args(args)

-     if len(args) < 1:

-         parser.error(_("Please specify a host"))

-         assert False  # pragma: no cover

-     activate_session(session)

-     for host in args:

-         if host.isdigit():

-             host = int(host)

-         info = session.getHost(host)

-         if info is None:

-             print("No such host: %s\n" % host)

-             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'))

-         update = session.getLastHostUpdate(info['id'])

-         if update is None:

-             update = "never"

-         else:

-             update = update[:update.find('.')]

-         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")

- 

- def handle_clone_tag(options, session, args):

-     "[admin] Duplicate the contents of one tag onto another tag"

-     usage = _("usage: %prog clone-tag [options] <src-tag> <dst-tag>")

-     usage += _("\n(Specify the --help global option for a list of other help options)")

-     parser = OptionParser(usage=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',

-             help=_('Clone tag at a specific timestamp'))

-     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("-f","--force", action="store_true",

-             help=_("override tag locks if necessary"))

-     parser.add_option("-n","--test", action="store_true", help=_("test mode"))

-     (options, args) = parser.parse_args(args)

- 

-     if len(args) != 2:

-         parser.error(_("This command takes two arguments: <src-tag> <dst-tag>"))

-         assert False  # pragma: no cover

-     activate_session(session)

- 

-     if not session.hasPerm('admin') and not options.test:

-         print(_("This action requires admin privileges"))

-         return

- 

-     if args[0] == args[1]:

-         sys.stdout.write('Source and destination tags must be different.\n')

-         return

- 

-     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.

-     srctag = session.getTag(args[0])

-     dsttag = session.getTag(args[1])

-     if not srctag:

-         sys.stdout.write("Unknown src-tag: %s\n" % args[0])

-         return

-     if (srctag['locked'] and not options.force) or (dsttag and dsttag['locked'] and not options.force):

-         print(_("Error: You are attempting to clone from or to a tag which is locked."))

-         print(_("Please use --force if this is what you really want to do."))

-         return

- 

-     # init debug lists.

-     chgpkglist=[]

-     chgbldlist=[]

-     chggrplist=[]

-     # case of brand new dst-tag.

-     if not dsttag:

-         if not options.config:

-             print(_('Cannot create tag without specifying --config'))

-             return

-         # create a new tag, copy srctag header.

-         if not options.test:

-             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'])

-             newtag = session.getTag(args[1]) # store the new tag, need its asigned id.

-         # 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:

-                 session.multiCall()

-         if options.builds:

-             # get --all latest builds from src tag

-             builds = 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:

-                 build['name'] = build['package_name'] # add missing 'name' field.

-                 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)

-             if not options.test:

-                 session.multiCall()

-         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:

-                 session.multiCall()

-     # case of existing dst-tag.

-     if dsttag:

-         # get fresh list of packages & builds into maps.

-         srcpkgs = {}

-         dstpkgs = {}

-         srclblds = {}

-         dstlblds = {}

-         srcgroups = {}

-         dstgroups = {}

-         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:

-             src_builds = session.listTagged(srctag['id'],

-                                             event=event.get('id'),

-                                             inherit=options.inherit_builds,

-                                             latest=options.latest_only)

-             for build in src_builds:

-                 srclblds[build['nvr']] = build

-             for build in session.getLatestBuilds(dsttag['name']):

-                 dstlblds[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

-         for (nvr, lbld) in six.iteritems(srclblds):

-             if nvr not in dstlblds:

-                 baddlist.append(lbld)

-         baddlist.sort(key = lambda x: x['package_name'])

-         bdellist = [] # list containing new builds to be removed from src tag

-         for (nvr, lbld) in six.iteritems(dstlblds):

-             if nvr not in srclblds:

-                 bdellist.append(lbld)

-         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 = {} # dict of changes to make in shared groups

-         for (grpname, group) in six.iteritems(srcgroups):

-             if grpname in dstgroups:

-                 grpchanges[grpname] = {'adds':[], 'dels':[]}

-                 # Store whether group is inherited or not

-                 grpchanges[grpname]['inherited'] = False

-                 if group['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 not pkg in dstgrppkglist:

-                         grpchanges[grpname]['adds'].append(pkg)

-                 for pkg in dstgrppkglist:

-                     if not pkg 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:

-             session.multiCall()

-         # ADD builds.

-         if not options.test:

-             session.multicall = True

-         for build in baddlist:

-             build['name'] = build['package_name'] # add missing 'name' field.

-             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)

-         if not options.test:

-             session.multiCall()

-         # 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:

-             session.multiCall()

-         # 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:

-             session.multiCall()

-         # DEL builds.

-         if not options.test:

-             session.multicall = True

-         for build in bdellist:

-             # dont delete an inherited build.

-             if build['tag_name'] == dsttag['name']:

-                 build['name'] = build['package_name'] # add missing 'name' field.

-                 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)

-         if not options.test:

-             session.multiCall()

-         # DEL packages.

-         if not options.test:

-             session.multicall = True

-         for pkg in pdellist:

-             # delete only non-inherited packages.

-             if build['tag_name'] == dsttag['name']:

-                 # check if package have owned builds inside.

-                 builds = session.listTagged(dsttag['name'], package=pkg['package_name'], inherit=False)

-                 #remove all its builds first if there are any.

-                 for build in builds:

-                     build['name'] = build['package_name'] #add missing 'name' field.

-                     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)

-                 # now safe to remove package itselfm 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.

-             if build['tag_name'] != dsttag['name']:

-                 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:

-             session.multiCall()

-         # 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:

-             session.multiCall()

-         # 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:

-             session.multiCall()

-     # 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', 'Latest 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(options, session, args):

-     "[admin] Create a new build target"

-     usage = _("usage: %prog add-target name build-tag <dest-tag>")

-     usage += _("\n(Specify the --help global option for a list of other help options)")

-     parser = OptionParser(usage=usage)

-     (options, args) = parser.parse_args(args)

-     if len(args) < 2:

-         parser.error(_("Please specify a target name, a build tag, and destination tag"))

-         assert False  # pragma: no cover

-     elif len(args) > 3:

-         parser.error(_("Incorrect number of arguments"))

-         assert False  # pragma: no cover

-     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)

-     if not session.hasPerm('admin'):

-         print("This action requires admin privileges")

-         return 1

- 

-     chkbuildtag = session.getTag(build_tag)

-     chkdesttag = session.getTag(dest_tag)

-     if not chkbuildtag:

-         print("Build tag does not exist: %s" % build_tag)

-         return 1

-     if not chkbuildtag.get("arches", None):

-         print("Build tag has no arches: %s" % build_tag)

-         return 1

-     if not chkdesttag:

-         print("Destination tag does not exist: %s" % dest_tag)

-         return 1

- 

-     session.createBuildTarget(name, build_tag, dest_tag)

- 

- def handle_edit_target(options, 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] name")

-     usage += _("\n(Specify the --help global option for a list of other help options)")

-     parser = OptionParser(usage=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"))

-         assert False  # pragma: no cover

-     activate_session(session)

- 

-     if not session.hasPerm('admin'):

-         print("This action requires admin privileges")

-         return

- 

-     targetInfo = session.getBuildTarget(args[0])

-     if targetInfo == None:

-         raise koji.GenericError("No build target with the name or id '%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:

-             print("Build tag does not exist: %s" % options.build_tag)

-             return 1

-         if not chkbuildtag.get("arches", None):

-             print("Build tag has no arches: %s" % options.build_tag)

-             return 1

-     if options.dest_tag:

-         chkdesttag = session.getTag(options.dest_tag)

-         if not chkdesttag:

-             print("Destination tag does not exist: %s" % options.dest_tag)

-             return 1

-         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(options, session, args):

-     "[admin] Remove a build target"

-     usage = _("usage: %prog remove-target [options] name")

-     usage += _("\n(Specify the --help global option for a list of other help options)")

-     parser = OptionParser(usage=usage)

-     (options, args) = parser.parse_args(args)

- 

-     if len(args) != 1:

-         parser.error(_("Please specify a build target to remove"))

-         assert False  # pragma: no cover

-     activate_session(session)

- 

-     if not session.hasPerm('admin'):

-         print("This action requires admin privileges")

-         return

- 

-     target = args[0]

-     target_info = session.getBuildTarget(target)

-     if not target_info:

-         print("Build target %s does not exist" % target)

-         return 1

- 

-     session.deleteBuildTarget(target_info['id'])

- 

- def handle_remove_tag(options, session, args):

-     "[admin] Remove a tag"

-     usage = _("usage: %prog remove-tag [options] name")

-     usage += _("\n(Specify the --help global option for a list of other help options)")

-     parser = OptionParser(usage=usage)

-     (options, args) = parser.parse_args(args)

- 

-     if len(args) != 1:

-         parser.error(_("Please specify a tag to remove"))

-         assert False  # pragma: no cover

-     activate_session(session)

- 

-     if not session.hasPerm('admin'):

-         print("This action requires admin privileges")

-         return

- 

-     tag = args[0]

-     tag_info = session.getTag(tag)

-     if not tag_info:

-         print("Tag %s does not exist" % tag)

-         return 1

- 

-     session.deleteTag(tag_info['id'])

- 

- def anon_handle_list_targets(options, session, args):

-     "[info] List the build targets"

-     usage = _("usage: %prog list-targets [options]")

-     usage += _("\n(Specify the --help global option for a list of other help options)")

-     parser = OptionParser(usage=usage)

-     parser.add_option("--name", help=_("Specify the build target name"))

-     parser.add_option("--quiet", action="store_true", help=_("Do not print the header information"), default=options.quiet)

-     (options, args) = parser.parse_args(args)

-     if len(args) != 0:

-         parser.error(_("This command takes no arguments"))

-         assert False  # pragma: no cover

-     activate_session(session)

- 

-     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 = [(x['name'], x) for x in session.getBuildTargets(options.name)]

-     tmp_list.sort()

-     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 == 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']])

- 

-     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(options, session, args):

-     "[info] Print the inheritance information for a tag"

-     usage = _("usage: %prog list-tag-inheritance [options] <tag>")

-     usage += _("\n(Specify the --help global option for a list of other help options)")

-     parser = OptionParser(usage=usage)

-     parser.add_option("--reverse", action="store_true", help=_("Process tag's children instead of its parents"))

-     parser.add_option("--stop", help=_("Stop processing inheritance at this tag"))

-     parser.add_option("--jump", help=_("Jump from one tag to another when processing inheritance"))

-     parser.add_option("--event", type='int', metavar="EVENT#", help=_("query at event"))

-     parser.add_option("--ts", type='int', metavar="TIMESTAMP", help=_("query at 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 exctly one argument: a tag name or ID"))

-         assert False  # pragma: no cover

-     activate_session(session)

-     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(_("Unknown tag: %s" % args[0]))

- 

-     opts = {}

-     opts['reverse'] = options.reverse or False

-     opts['stops'] = {}

-     opts['jumps'] = {}

-     if event:

-         opts['event'] = event['id']

- 

-     if options.jump:

-         match = re.match(r'^(.*)/(.*)$', options.jump)

-         if match:

-             tag1 = session.getTagID(match.group(1))

-             if not tag1:

-                 parser.error(_("Unknown tag: %s" % match.group(1)))

-             tag2 = session.getTagID(match.group(2))

-             if not tag2:

-                 parser.error(_("Unknown tag: %s" % match.group(2)))

-             opts['jumps'][str(tag1)] = tag2

- 

-     if options.stop:

-         tag1 = session.getTagID(options.stop)

-         if not tag1:

-             parser.error(_("Unknown tag: %s" % options.stop))

-         opts['stops'] = {str(tag1): 1}

- 

-     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(options, session, args):

-     "[info] Print the list of tags"

-     usage = _("usage: %prog list-tags [options] [pattern]")

-     usage += _("\n(Specify the --help global option for a list of other help options)")

-     parser = OptionParser(usage=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, args) = parser.parse_args(args)

-     activate_session(session)

- 

-     pkginfo = {}

-     buildinfo = {}

- 

-     if options.package:

-         pkginfo = session.getPackage(options.package)

-         if not pkginfo:

-             parser.error(_("Invalid package %s" % options.package))

-             assert False  # pragma: no cover

- 

-     if options.build:

-         buildinfo = session.getBuild(options.build)

-         if not buildinfo:

-             parser.error(_("Invalid build %s" % options.build))

-             assert False  # pragma: no cover

- 

-     tags = session.listTags(buildinfo.get('id',None), pkginfo.get('id',None))

-     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 args:

-             for pattern in args:

-                 if fnmatch.fnmatch(tag['name'], pattern):

-                     break

-             else:

-                 continue

-         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 anon_handle_list_tag_history(options, session, args):

-     "[info] Print a history of tag operations"

-     usage = _("usage: %prog list-tag-history [options]")

-     usage += _("\n(Specify the --help global option for a list of other help options)")

-     parser = OptionParser(usage=usage)

-     parser.add_option("--debug", action="store_true")

-     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("--all", action="store_true", help=_("Allows listing the entire global history"))

-     (options, args) = parser.parse_args(args)

-     if len(args) != 0:

-         parser.error(_("This command takes no arguments"))

-         assert False  # pragma: no cover

-     kwargs = {}

-     limited = False

-     if options.package:

-         kwargs['package'] = options.package

-         limited = True

-     if options.tag:

-         kwargs['tag'] = options.tag

-         limited = True

-     if options.build:

-         kwargs['build'] = options.build

-         limited = True

-     if not limited and not options.all:

-         parser.error(_("Please specify an option to limit the query"))

- 

-     activate_session(session)

- 

-     hist = session.tagHistory(**kwargs)

-     timeline = []

-     for x in hist:

-         event_id = x['revoke_event']

-         if event_id is not None:

-             timeline.append((event_id, x))

-         event_id = x['create_event']

-         timeline.append((event_id, x))

-     timeline.sort()

-     def _histline(event_id, x):

-         if event_id == x['revoke_event']:

-             ts = x['revoke_ts']

-             fmt = "%(name)s-%(version)s-%(release)s untagged from %(tag_name)s"

-             if 'revoker_name' in x:

-                 fmt += " by %(revoker_name)s"

-         elif event_id == x['create_event']:

-             ts = x['create_ts']

-             fmt = "%(name)s-%(version)s-%(release)s tagged into %(tag_name)s"

-             if 'creator_name' in x:

-                 fmt += " by %(creator_name)s"

-             if x['active']:

-                 fmt += " [still active]"

-         else:

-             raise koji.GenericError("unknown event: (%r, %r)" % (event_id, x))

-         time_str = time.asctime(time.localtime(ts))

-         return "%s: %s" % (time_str, fmt % x)

-     for event_id, x in timeline:

-         if options.debug:

-             print("%r" % x)

-         print(_histline(event_id, x))

- 

- 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:

-             print("Warning: 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_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 == '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

-     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 = list(x.keys())

-         keys.sort()

-         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 = list(x.keys())

-         keys.sort()

-         # 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'],

-     'tag_external_repos' : ['tag_id', 'external_repo_id'],

-     'tag_listing' : ['build_id', 'tag_id'],

-     'tag_packages' : ['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(options, session, args):

-     "[info] Display historical data"

-     usage = _("usage: %prog list-history [options]")

-     usage += _("\n(Specify the --help global option for a list of other help options)")

-     parser = OptionParser(usage=usage)

-     parser.add_option("--debug", action="store_true")

-     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 permission"))

-     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("--before", metavar="TIMESTAMP", help=_("Only show entries before timestamp"))

-     parser.add_option("--after", metavar="TIMESTAMP", help=_("Only show entries after timestamp"))

-     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"))

-     global_options = options

-     (options, args) = parser.parse_args(args)

-     if len(args) != 0:

-         parser.error(_("This command takes no arguments"))

-         assert False  # pragma: no cover

-     kwargs = {}

-     limited = False

-     for opt in ('before', 'after'):

-         val = getattr(options, opt)

-         if not val:

-             continue

-         try:

-             ts = float(val)

-             setattr(options, opt, ts)

-             continue

-         except ValueError:

-             pass

-         try:

-             dt = dateutil.parser.parse(val)

-             ts = time.mktime(dt.timetuple())

-             setattr(options, opt, ts)

-         except:

-             parser.error(_("Invalid time specification: %s") % val)

-     for opt in ('package', 'tag', 'build', 'editor', 'user', 'permission',

-                 'cg', 'external_repo', 'build_target', 'group', 'before',

-                 'after'):

-         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

-     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"))

- 

-     activate_session(session)

- 

-     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()

-         #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(global_options.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):

-     try:

-         return _do_parseTaskParams(session, method, task_id)

-     except Exception:

-         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):

-     """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" % (options.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['creation_time']))

-         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, 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):

-             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(cmp=lambda a, b: cmp(a['id'], b['id']))

-         for child in children:

-             _printTaskInfo(session, child['id'], level, verbose=verbose)

- 

- def anon_handle_taskinfo(options, session, args):

-     """[info] Show information about a task"""

-     usage = _("usage: %prog taskinfo [options] taskID [taskID...]")

-     usage += _("\n(Specify the --help global option for a list of other help options)")

-     parser = OptionParser(usage=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"))

-         assert False  # pragma: no cover

- 

-     activate_session(session)

- 

-     for arg in args:

-         task_id = int(arg)

-         _printTaskInfo(session, task_id, 0, options.recurse, options.verbose)

- 

- def anon_handle_taginfo(options, session, args):

-     "[info] Print basic information about a tag"

-     usage = _("usage: %prog taginfo [options] <tag> [<tag> ...]")

-     usage += _("\n(Specify the --help global option for a list of other help options)")

-     parser = OptionParser(usage=usage)

-     parser.add_option("--event", type='int', metavar="EVENT#", help=_("query at event"))

-     parser.add_option("--ts", type='int', metavar="TIMESTAMP", help=_("query at 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"))

-         assert False  # pragma: no cover

-     activate_session(session)

-     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.getTag(tag, **event_opts)

-         if info is None:

-             print("No such tag: %s" % tag)

-             sys.exit(1)

-         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 = [x['name'] for x in session.getTagGroups(info['id'], **event_opts)]

-         group_list.sort()

-         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:")

-             keys = list(info['extra'].keys())

-             keys.sort()

-             for key in keys:

-                 print("  %s : %s" % (key, pprint.pformat(info['extra'][key])))

-         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:

-                 print("  %(priority)3i %(external_repo_name)s (%(url)s)" % rinfo)

-         print("Inheritance:")

-         for parent in session.getInheritanceData(tag, **event_opts):

-             flags = ''

-             for code,expr in (

-                     ('M',parent['maxdepth'] is not None),

-                     ('F',parent['pkg_filter']),

-                     ('I',parent['intransitive']),

-                     ('N',parent['noconfig']),):

-                 if expr:

-                     flags += code

-                 else:

-                     flags += '.'

-             parent['flags'] = flags

-             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(options, session, args):

-     "[admin] Add a new tag to the database"

-     usage = _("usage: %prog add-tag [options] name")

-     usage += _("\n(Specify the --help global option for a list of other help options)")

-     parser = OptionParser(usage=usage)

-     parser.add_option("--parent", help=_("Specify parent"))

-     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"))

-         assert False  # pragma: no cover

-     activate_session(session)

-     if not session.hasPerm('admin'):

-         print("This action requires admin privileges")

-         return

-     opts = {}

-     if options.parent:

-         opts['parent'] = options.parent

-     if options.arches:

-         opts['arches'] = 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(options, session, args):

-     "[admin] Alter tag information"

-     usage = _("usage: %prog edit-tag [options] name")

-     usage += _("\n(Specify the --help global option for a list of other help options)")

-     parser = OptionParser(usage=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"))

-     parser.add_option("-r", "--remove-extra", action="append", default=[], metavar="key",

-                       help=_("Remove tag extra option"))

-     (options, args) = parser.parse_args(args)

-     if len(args) != 1:

-         parser.error(_("Please specify a name for the tag"))

-         assert False  # pragma: no cover

-     activate_session(session)

-     tag = args[0]

-     opts = {}

-     if options.arches:

-         opts['arches'] = parse_arches(options.arches)

-     if options.no_perm:

-         opts['perm_id'] = 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)

-             value = arg_filter(value)

-             extra[key] = value

-         opts['extra'] = extra

-     if options.remove_extra:

-         opts['remove_extra'] = options.remove_extra

-     #XXX change callname

-     session.editTag2(tag, **opts)

- 

- def handle_lock_tag(options, session, args):

-     "[admin] Lock a tag"

-     usage = _("usage: %prog lock-tag [options] <tag> [<tag> ...] ")

-     usage += _("\n(Specify the --help global option for a list of other help options)")

-     parser = OptionParser(usage=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"))

-         assert False  # pragma: no cover

-     activate_session(session)

-     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) 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=perm_id)

- 

- def handle_unlock_tag(options, session, args):

-     "[admin] Unlock a tag"

-     usage = _("usage: %prog unlock-tag [options] <tag> [<tag> ...]")

-     usage += _("\n(Specify the --help global option for a list of other help options)")

-     parser = OptionParser(usage=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"))

-         assert False  # pragma: no cover

-     activate_session(session)

-     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)

-                 assert False  # pragma: no cover

-             selected.append(tag)

-         selected = [session.getTag(name) for name in args]

-     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'], locked=False, perm_id=None)

- 

- def handle_add_tag_inheritance(options, session, args):

-     """[admin] Add to a tag's inheritance"""

-     usage = _("usage: %prog add-tag-inheritance [options] tag parent-tag")

-     usage += _("\n(Specify the --help global option for a list of other help options)")

-     parser = OptionParser(usage=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", 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"))

-         assert False  # pragma: no cover

- 

-     activate_session(session)

- 

-     tag = session.getTag(args[0])

-     if not tag:

-         parser.error(_("Invalid tag: %s" % args[0]))

- 

-     parent = session.getTag(args[1])

-     if not parent:

-         parser.error(_("Invalid 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:

-         print(_("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']))

-         print(_("Please use --force if this is what you really want to do."))

-         return

-     if samePriority:

-         print(_("Error: There is already an active inheritance with that priority on %s, please specify a different priority with --priority." % tag['name']))

-         return

- 

-     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(options, session, args):

-     """[admin] Edit tag inheritance"""

-     usage = _("usage: %prog edit-tag-inheritance [options] tag <parent> <priority>")

-     usage += _("\n(Specify the --help global option for a list of other help options)")

-     parser = OptionParser(usage=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 lease one argument: a tag name or ID"))

-         assert False  # pragma: no cover

- 

-     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"))

-         assert False  # pragma: no cover

- 

-     activate_session(session)

- 

-     tag = session.getTag(args[0])

-     if not tag:

-         parser.error(_("Invalid tag: %s" % args[0]))

- 

-     parent = None

-     priority = None

-     if len(args) > 1:

-         parent = session.getTag(args[1])

-         if not parent:

-             parser.error(_("Invalid 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:

-         print(_("No inheritance link found to remove.  Please check your arguments"))

-         return 1

-     elif len(data) > 1:

-         print(_("Multiple matches for tag."))

-         if not parent:

-             print(_("Please specify a parent on the command line."))

-             return 1

-         if not priority:

-             print(_("Please specify a priority on the command line."))

-             return 1

-         print(_("Error: Key constraints may be broken.  Exiting."))

-         return 1

- 

-     # len(data) == 1

-     data = data[0]

- 

-     inheritanceData = session.getInheritanceData(tag['id'])

-     samePriority = [datum for datum in inheritanceData if datum['priority'] == options.priority]

-     if samePriority:

-         print(_("Error: There is already an active inheritance with that priority on %s, please specify a different priority with --priority.") % tag['name'])

-         return 1

- 

-     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:

-             print(_("Invalid maxdepth: %s") % options.maxdepth)

-             return 1

-     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(options, session, args):

-     """[admin] Remove a tag inheritance link"""

-     usage = _("usage: %prog remove-tag-inheritance tag <parent> <priority>")

-     usage += _("\n(Specify the --help global option for a list of other help options)")

-     parser = OptionParser(usage=usage)

-     (options, args) = parser.parse_args(args)

- 

-     if len(args) < 1:

-         parser.error(_("This command takes at lease one argument: a tag name or ID"))

-         assert False  # pragma: no cover

- 

-     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"))

-         assert False  # pragma: no cover

- 

-     activate_session(session)

- 

-     tag = session.getTag(args[0])

-     if not tag:

-         parser.error(_("Invalid tag: %s" % args[0]))

- 

-     parent = None

-     priority = None

-     if len(args) > 1:

-         parent = session.getTag(args[1])

-         if not parent:

-             parser.error(_("Invalid 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:

-         print(_("No inheritance link found to remove.  Please check your arguments"))

-         return

-     elif len(data) > 1:

-         print(_("Multiple matches for tag."))

-         if not parent:

-             print(_("Please specify a parent on the command line."))

-             return

-         if not priority:

-             print(_("Please specify a priority on the command line."))

-             return

-         print(_("Error: Key constrainsts may be broken.  Exiting."))

-         return

- 

-     # 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(options, session, args):

-     "[info] Show groups data for a tag"

-     usage = _("usage: %prog show-groups [options] <tag>")

-     usage += _("\n(Specify the --help global option for a list of other help options)")

-     parser = OptionParser(usage=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"))

-     (options, args) = parser.parse_args(args)

-     if len(args) != 1:

-         parser.error(_("Incorrect number of arguments"))

-         assert False  # pragma: no cover

-     activate_session(session)

-     tag = args[0]

-     groups = session.getTagGroups(tag)

-     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(options, session, args):

-     "[info] List external repos"

-     usage = _("usage: %prog list-external-repos [options]")

-     usage += _("\n(Specify the --help global option for a list of other help options)")

-     parser = OptionParser(usage=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 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=options.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"))

-         assert False  # pragma: no cover

-     activate_session(session)

-     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"))

-                 assert False  # pragma: no cover

-         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 %(url)s"

-         header1 = "%-3s %-25s %s" % ("Pri", "External repo name", "URL")

-         header2 = "%s %s %s" % ("-"*3, "-"*25, "-"*40)

-     elif format == "multitag":

-         format = "%(tag_name)-20s %(priority)-3i %(external_repo_name)s"

-         header1 = "%-20s %-3s %s" % ("Tag", "Pri", "External repo name")

-         header2 = "%s %s %s" % ("-"*20, "-"*3, "-"*25)

-     if not options.quiet:

-         print(header1)

-         print(header2)

-     for rinfo in data:

-         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(options, session, args):

-     "[admin] Create an external repo and/or add one to a tag"

-     usage = _("usage: %prog add-external-repo [options] name [url]")

-     usage += _("\n(Specify the --help global option for a list of other help options)")

-     parser = OptionParser(usage=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)"))

-     (options, args) = parser.parse_args(args)

-     activate_session(session)

-     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"))

-         assert False  # pragma: no cover

-     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)

-             session.addExternalRepoToTag(tag, rinfo['name'], priority)

-             print("Added external repo %s to tag %s (priority %i)" \

-                     % (rinfo['name'], tag, priority))

- 

- def handle_edit_external_repo(options, session, args):

-     "[admin] Edit data for an external repo"

-     usage = _("usage: %prog edit-external-repo name")

-     usage += _("\n(Specify the --help global option for a list of other help options)")

-     parser = OptionParser(usage=usage)

-     parser.add_option("--url",  help=_("Change the url"))

-     parser.add_option("--name",  help=_("Change the name"))

-     (options, args) = parser.parse_args(args)

-     if len(args) != 1:

-         parser.error(_("Incorrect number of arguments"))

-         parser.error(_("This command takes no arguments"))

-         assert False  # pragma: no cover

-     opts = {}

-     if options.url:

-         opts['url'] = options.url

-     if options.name:

-         opts['name'] = options.name

-     if not opts:

-         parser.error(_("No changes specified"))

-     activate_session(session)

-     session.editExternalRepo(args[0], **opts)

- 

- def handle_remove_external_repo(options, session, args):

-     "[admin] Remove an external repo from a tag or tags, or remove entirely"

-     usage = _("usage: %prog remove-external-repo repo [tag ...]")

-     usage += _("\n(Specify the --help global option for a list of other help options)")

-     parser = OptionParser(usage=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"))

-         assert False  # pragma: no cover

-     activate_session(session)

-     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"))

-             assert False  # pragma: no cover

-         if not current_tags:

-             print(_("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:

-             print(_("Error: external repo %s used by tag(s): %s") % (repo, ', '.join(current_tags)))

-             print(_("Use --force to remove anyway"))

-             return 1

-         session.deleteExternalRepo(args[0])

-     else:

-         for tag in tags:

-             if not tag 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] <name> <version> <target>" +

-               " <arch> <kickstart-file>")

-     usage += _("\n(Specify the --help global option for a list of other " +

-                "help options)")

-     parser = OptionParser(usage=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("--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."))

-         assert False  # pragma: no cover

-     _build_image(options, task_options, session, args, 'livecd')

- 

- 

- # This handler is for spinning livemedia images

- def handle_spin_livemedia(options, session, args):

-     """[admin] Create a livemedia image given a kickstart file"""

- 

-     # Usage & option parsing.

-     usage = _("usage: %prog spin-livemedia [options] <name> <version> <target>" +

-               " <arch> <kickstart-file>")

-     usage += _("\n(Specify the --help global option for a list of other " +

-                "help options)")

-     parser = OptionParser(usage=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("--title", help=_("Set the image title (defaults to <name>)"))

-     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."))

-     (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."))

-         assert False  # pragma: no cover

-     _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] <name> <version> " +

-               "<target> <arch> <kickstart-file>")

-     usage += _("\n(Specify the --help global option for a list of other " +

-                "help options)")

-     parser = OptionParser(usage=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."))

-         assert False  # pragma: no cover

-     _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 FILE")

-     usage += _("\n\n(Specify the --help global option for a list of other " +

-                "help options)")

-     parser = OptionParser(usage=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("--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)

- 

-     # 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(_("Unknown build target: %s" % tmp_target))

-     dest_tag = session.getTag(tmp_target['dest_tag'])

-     if not dest_tag:

-         raise koji.GenericError(_("Unknown 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)

-     #else:

-     #    return

- 

- 

- 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] <name> <version> " +

-               "<target> <install-tree-url> <arch> [<arch>...]")

-     usage += _("\n       %prog image-build --config FILE")

-     usage += _("\n\n(Specify the --help global option for a list of other " +

-                "help options)")

-     parser = OptionParser(usage=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 " % ', '.join(formats) +

-                "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."))

-     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("--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:

-         if not os.path.exists(task_options.config):

-             parser.error(_("%s not found!" % task_options.config))

-         section = 'image-build'

-         config = six.moves.configparser.ConfigParser()

-         conf_fd = open(task_options.config)

-         config.readfp(conf_fd)

-         conf_fd.close()

-         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"))

-     _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)

- 

-     # 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(_("Unknown build target: %s" % target))

-     dest_tag = session.getTag(tmp_target['dest_tag'])

-     if not dest_tag:

-         raise koji.GenericError(_("Unknown 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 = {}

-     if hasattr(task_opts, 'optional_arches'):

-         hub_opts['optional_arches'] = task_opts.optional_arches.split(',')

-     passthru_opts = [

-         'format', 'install_tree_url', 'isoname', 'ksurl',

-         'ksversion', 'release', 'repo', 'scratch', 'skip_tag',

-         'specfile', 'title', 'vcpu', 'vmem', 'optional_arches',

-         ]

-     for opt in passthru_opts:

-         val = getattr(task_opts, opt, None)

-         if val is not None:

-             hub_opts[opt] = val

- 

-     # 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)

-     else:

-         return

- 

- 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)

- 

-     # 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(_("Unknown build target: %s" % target))

-     dest_tag = session.getTag(tmp_target['dest_tag'])

-     if not dest_tag:

-         raise koji.GenericError(_("Unknown 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'):

-         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)

-     else:

-         return

- 

- def handle_win_build(options, session, args):

-     """[build] Build a Windows package from source"""

-     # Usage & option parsing

-     usage = _("usage: %prog win-build [options] target URL VM")

-     usage += _("\n(Specify the --help global option for a list of other " +

-                "help options)")

-     parser = OptionParser(usage=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"))

-         assert False  # pragma: no cover

-     activate_session(session)

-     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(_("Unknown build target: %s" % target))

-         dest_tag = session.getTag(build_target['dest_tag'])

-         if not dest_tag:

-             parser.error(_("Unknown 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)

-     else:

-         return

- 

- def handle_free_task(options, session, args):

-     "[admin] Free a task"

-     usage = _("usage: %prog free-task [options] <task-id> [<task-id> ...]")

-     usage += _("\n(Specify the --help global option for a list of other help options)")

-     parser = OptionParser(usage=usage)

-     (options, args) = parser.parse_args(args)

-     activate_session(session)

-     tlist = []

-     for task_id in args:

-         try:

-             tlist.append(int(task_id))

-         except ValueError:

-             parser.error(_("task-id must be an integer"))

-             assert False  # pragma: no cover

-     for task_id in tlist:

-         session.freeTask(task_id)

- 

- def handle_cancel(options, session, args):

-     "[build] Cancel tasks and/or builds"

-     usage = _("usage: %prog cancel [options] <task-id|build> [<task-id|build> ...]")

-     usage += _("\n(Specify the --help global option for a list of other help options)")

-     parser = OptionParser(usage=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"))

-         assert False  # pragma: no cover

-     activate_session(session)

-     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)"))

-                 assert False  # pragma: no cover

-     if tlist:

-         opts = {}

-         remote_fn = session.cancelTask

-         if options.justone:

-             opts['recurse'] = False

-         elif options.full:

-             remote_fn = session.cancelTaskFull

-             if options.force:

-                 opts['strict'] = False

-         for task_id in tlist:

-             remote_fn(task_id, **opts)

-     for build in blist:

-         session.cancelBuild(build)

- 

- def handle_set_task_priority(options, session, args):

-     "[admin] Set task priority"

-     usage = _("usage: %prog set-task-priority [options] --priority=<priority> <task-id> [task-id]...")

-     usage += _("\n(Specify the --help global option for a list of other help options)")

-     parser = OptionParser(usage=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"))

-         assert False  # pragma: no cover

- 

-     if options.priority is None:

-         parser.error(_("You must specify --priority"))

-         assert False  # pragma: no cover

-     try:

-         tasks = [int(a) for a in args]

-     except ValueError:

-         parser.error(_("Task numbers must be integers"))

- 

-     activate_session(session)

- 

-     for task_id in tasks:

-         session.setTaskPriority(task_id, options.priority, options.recurse)

- 

- def _list_tasks(options, session):

-     "Retrieve a list of tasks"

- 

-     callopts = {

-         'state' : [koji.TASK_STATES[s] for s in ('FREE', 'OPEN', 'ASSIGNED')],

-         'decode' : True,

-     }

- 

-     if options.mine:

-         user = session.getLoggedInUser()

-         if not user:

-             print("Unable to determine user")

-             sys.exit(1)

-         callopts['owner'] = user['id']

-     if options.user:

-         user = session.getUser(options.user)

-         if not user:

-             print("No such user: %s" % options.user)

-             sys.exit(1)

-         callopts['owner'] = user['id']

-     if options.arch:

-         callopts['arch'] = parse_arches(options.arch, to_list=True)

-     if options.method:

-         callopts['method'] = options.method

-     if options.channel:

-         chan = session.getChannel(options.channel)

-         if not chan:

-             print("No such channel: %s" % options.channel)

-             sys.exit(1)

-         callopts['channel_id'] = chan['id']

-     if options.host:

-         host = session.getHost(options.host)

-         if not host:

-             print("No such host: %s" % options.host)

-             sys.exit(1)

-         callopts['host_id'] = host['id']

- 

-     qopts = {'order' : 'priority,create_time'}

-     tasklist = session.listTasks(callopts, qopts)

-     tasks = dict([(x['id'], x) for x in tasklist])

- 

-     #thread the tasks

-     for t in tasklist:

-         if t['parent'] is not None:

-             parent = tasks.get(t['parent'])

-             if parent:

-                 parent.setdefault('children',[])

-                 parent['children'].append(t)

-                 t['sub'] = True

- 

-     return tasklist

- 

- 

- def handle_list_tasks(options, session, args):

-     "[info] Print the list of tasks"

-     usage = _("usage: %prog list-tasks [options]")

-     usage += _("\n(Specify the --help global option for a list of other help options)")

-     parser = OptionParser(usage=usage)

-     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", help=_("Do not display the column headers"), default=options.quiet)

-     (options, args) = parser.parse_args(args)

-     if len(args) != 0:

-         parser.error(_("This command takes no arguments"))

-         assert False  # pragma: no cover

- 

-     activate_session(session)

-     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(options, session, args):

-     "[admin] Set the list of extra arches for a package"

-     usage = _("usage: %prog set-pkg-arches [options] arches tag package [package2 ...]")

-     usage += _("\n(Specify the --help global option for a list of other help options)")

-     parser = OptionParser(usage=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"))

-         assert False  # pragma: no cover

-     activate_session(session)

-     arches = parse_arches(args[0])

-     tag = args[1]

-     for package in args[2:]:

-         #really should implement multicall...

-         session.packageListSetArches(tag,package,arches,force=options.force)

- 

- def handle_set_pkg_owner(options, session, args):

-     "[admin] Set the owner for a package"

-     usage = _("usage: %prog set-pkg-owner [options] owner tag package [package2 ...]")

-     usage += _("\n(Specify the --help global option for a list of other help options)")

-     parser = OptionParser(usage=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"))

-         assert False  # pragma: no cover

-     activate_session(session)

-     owner = args[0]

-     tag = args[1]

-     for package in args[2:]:

-         #really should implement multicall...

-         session.packageListSetOwner(tag,package,owner,force=options.force)

- 

- def handle_set_pkg_owner_global(options, session, args):

-     "[admin] Set the owner for a package globally"

-     usage = _("usage: %prog set-pkg-owner-global [options] owner package [package2 ...]")

-     usage += _("\n(Specify the --help global option for a list of other help options)")

-     parser = OptionParser(usage=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"))

-             assert False  # pragma: no cover

-     elif len(args) < 2:

-         parser.error(_("Please specify an owner and at least one package"))

-         assert False  # pragma: no cover

-     activate_session(session)

-     owner = args[0]

-     packages = args[1:]

-     user = session.getUser(owner)

-     if not user:

-         print("No such user: %s" % owner)

-         return 1

-     opts = {'with_dups' : True}

-     old_user = None

-     if options.old_user:

-         old_user = session.getUser(options.old_user)

-         if not old_user:

-             print("No such user: %s" % options.old_user)

-             return 1

-         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:

-             print("No data for user %s" % old_user['name'])

-             return 1

-         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(options, session, args):

-     "[monitor] Track progress of particular tasks"

-     usage = _("usage: %prog watch-task [options] <task id> [<task id>...]")

-     usage += _("\n(Specify the --help global option for a list of other help options)")

-     parser = OptionParser(usage=usage)

-     parser.add_option("--quiet", action="store_true",

-                       help=_("Do not print the task information"), default=options.quiet)

-     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"))

- 

-     activate_session(session)

-     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)

- 

- def anon_handle_watch_logs(options, session, args):

-     "[monitor] Watch logs in realtime"

-     usage = _("usage: %prog watch-logs [options] <task id> [<task id>...]")

-     usage += _("\n(Specify the --help global option for a list of other help options)")

-     parser = OptionParser(usage=usage)

-     parser.add_option("--log", help=_("Watch only a specific log"))

-     (options, args) = parser.parse_args(args)

-     activate_session(session)

- 

-     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)

- 

- def handle_make_task(opts, session, args):

-     "[admin] Create an arbitrary task"

-     usage = _("usage: %prog make-task [options] <arg1> [<arg2>...]")

-     usage += _("\n(Specify the --help global option for a list of other help options)")

-     parser = OptionParser(usage=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)

-     activate_session(session)

- 

-     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=opts.quiet)

- 

- def handle_tag_build(opts, session, args):

-     "[bind] Apply a tag to one or more builds"

-     usage = _("usage: %prog tag-build [options] <tag> <pkg> [<pkg>...]")

-     usage += _("\n(Specify the --help global option for a list of other help options)")

-     parser = OptionParser(usage=usage)

-     parser.add_option("--force", action="store_true", help=_("force operation"))

-     parser.add_option("--nowait", action="store_true", 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"))

-         assert False  # pragma: no cover

-     activate_session(session)

-     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 _running_in_bg() or options.nowait:

-         return

-     else:

-         session.logout()

-         return watch_tasks(session,tasks,quiet=opts.quiet)

- 

- def handle_move_build(opts, session, args):

-     "[bind] 'Move' one or more builds between tags"

-     usage = _("usage: %prog move-build [options] <tag1> <tag2> <pkg> [<pkg>...]")

-     usage += _("\n(Specify the --help global option for a list of other help options)")

-     parser = OptionParser(usage=usage)

-     parser.add_option("--force", action="store_true", help=_("force operation"))

-     parser.add_option("--nowait", action="store_true", help=_("do not wait on tasks"))

-     parser.add_option("--all", action="store_true", help=_("move all instances of a package, <pkg>'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"))

-         assert False  # pragma: no cover

-     activate_session(session)

-     tasks = []

-     builds = []

- 

-     if options.all:

-         for arg in args[2:]:

-             pkg = session.getPackage(arg)

-             if not pkg:

-                 print(_("Invalid package name %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(_("Invalid build %s, skipping." % arg))

-                 continue

-             if not build 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 _running_in_bg() or options.nowait:

-         return

-     else:

-         session.logout()

-         return watch_tasks(session, tasks, quiet=opts.quiet)

- 

- def handle_untag_build(options, session, args):

-     "[bind] Remove a tag from one or more builds"

-     usage = _("usage: %prog untag-build [options] <tag> <pkg> [<pkg>...]")

-     usage += _("\n(Specify the --help global option for a list of other help options)")

-     parser = OptionParser(usage=usage)

-     parser.add_option("--all", action="store_true", help=_("untag all versions of the package in this tag"))

-     parser.add_option("--non-latest", action="store_true", help=_("untag all versions of the package in this tag except the latest"))

-     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"))

-             assert False  # pragma: no cover

-     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"))

-         assert False  # pragma: no cover

-     activate_session(session)

-     tag = session.getTag(args[0])

-     if not tag:

-         parser.error(_("Invalid tag: %s" % args[0]))

-     if options.all:

-         builds = []

-         for pkg in args[1:]:

-             builds.extend(session.listTagged(args[0], package=pkg))

-     elif options.non_latest:

-         if options.force and len(args) == 1:

-             tagged = session.listTagged(args[0])

-         else:

-             tagged = []

-             for pkg in args[1:]:

-                 tagged.extend(session.listTagged(args[0], package=pkg))

-         # listTagged orders entries latest first

-         seen_pkg = {}

-         builds = []

-         for binfo in tagged:

-             if binfo['name'] not in seen_pkg:

-                 #latest for this package

-                 if options.verbose:

-                     print(_("Leaving latest build for package %(name)s: %(nvr)s") % binfo)

-             else:

-                 builds.append(binfo)

-             seen_pkg[binfo['name']] = 1

-     else:

-         tagged = session.listTagged(args[0])

-         idx = dict([(b['nvr'], b) for b in tagged])

-         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:

-                     print(_("No such build: %s") % nvr)

-                 else:

-                     print(_("Build %s not in tag %s") % (nvr, tag['name']))

-                 if not options.force:

-                     return 1

-     builds.reverse()

-     for binfo in builds:

-         if options.test:

-             print(_("would have untagged %(nvr)s") % binfo)

-         else:

-             if options.verbose:

-                 print(_("untagging %(nvr)s") % binfo)

-             session.untagBuild(tag['name'], binfo['nvr'], force=options.force)

- 

- def handle_unblock_pkg(options, session, args):

-     "[admin] Unblock a package in the listing for tag"

-     usage = _("usage: %prog unblock-pkg [options] tag package [package2 ...]")

-     usage += _("\n(Specify the --help global option for a list of other help options)")

-     parser = OptionParser(usage=usage)

-     (options, args) = parser.parse_args(args)

-     if len(args) < 2:

-         parser.error(_("Please specify a tag and at least one package"))

-         assert False  # pragma: no cover

-     activate_session(session)

-     tag = args[0]

-     for package in args[1:]:

-         #really should implement multicall...

-         session.packageListUnblock(tag,package)

- 

- def anon_handle_download_build(options, session, args):

-     "[download] Download a built package"

-     usage = _("usage: %prog download-build [options] <n-v-r | build_id | package>")

-     usage += _("\n(Specify the --help global option for a list of other help options)")

-     parser = OptionParser(usage=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, or image)"))

-     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("-q", "--quiet", action="store_true", help=_("Do not display progress meter"),

-                       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"))

-         assert False  # pragma: no cover

-     elif len(args) > 1:

-         parser.error(_("Only a single package N-V-R or build ID may be specified"))

-         assert False  # pragma: no cover

- 

-     activate_session(session)

-     build = args[0]

- 

-     if build.isdigit():

-         if suboptions.latestfrom:

-             print("--latestfrom not compatible with build IDs, specify a package name.")

-             return 1

-         build = int(build)

-         if suboptions.task_id:

-             builds = session.listBuilds(taskID=build)

-             if not builds:

-                 print("No associated builds for task %s" % build)

-                 return 1

-             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:

-             print("Error finding latest build: %s" % data)

-             return 1

-         if not builds:

-             print("%s has no builds of %s" % (suboptions.latestfrom, build))

-             return 1

-         info = builds[0]

-     elif suboptions.rpm:

-         rpminfo = session.getRPM(build)

-         if rpminfo is None:

-             print("No such rpm: %s" % build)

-             return 1

-         info = session.getBuild(rpminfo['build_id'])

-     else:

-         # if we're given an rpm name without --rpm, download the containing build

-         try:

-             nvra = koji.parse_NVRA(build)

-             rpminfo = session.getRPM(build)

-             build = rpminfo['build_id']

-         except Exception:

-             pass

-         info = session.getBuild(build)

- 

-     if info is None:

-         print("No such build: %s" % build)

-         return 1

- 

-     if not suboptions.topurl:

-         print("You must specify --topurl to download files")

-         return 1

-     pathinfo = koji.PathInfo(topdir=suboptions.topurl)

- 

-     urls = []

-     if suboptions.type:

-         archives = session.listArchives(buildID=info['id'], type=suboptions.type)

-         if not archives:

-             print("No %s archives available for %s" % (suboptions.type, koji.buildLabel(info)))

-             return 1

-         if suboptions.type == 'maven':

-             for archive in archives:

-                 url = pathinfo.mavenbuild(info) + '/' + pathinfo.mavenfile(archive)

-                 urls.append((url, pathinfo.mavenfile(archive)))

-         elif suboptions.type == 'win':

-             for archive in archives:

-                 url = pathinfo.winbuild(info) + '/' + pathinfo.winfile(archive)

-                 urls.append((url, pathinfo.winfile(archive)))

-         elif suboptions.type == 'image':

-             if not suboptions.topurl:

-                 print("You must specify --topurl to download images")

-                 return 1

-             pi = koji.PathInfo(topdir=suboptions.topurl)

-             for archive in archives:

-                 url = '%s/%s' % (pi.imagebuild(info), archive['filename'])

-                 urls.append((url, archive['filename']))

-         else:

-             # can't happen

-             assert False  # pragma: no cover

-     else:

-         arches = suboptions.arches

-         if len(arches) == 0:

-             arches = None

-         if suboptions.rpm:

-             rpms = [rpminfo]

-         else:

-             rpms = session.listRPMs(buildID=info['id'], arches=arches)

-         if not rpms:

-             if arches:

-                 print("No %s packages available for %s" % (" or ".join(arches), koji.buildLabel(info)))

-             else:

-                 print("No packages available for %s" % koji.buildLabel(info))

-             return 1

-         for rpm in rpms:

-             if not suboptions.debuginfo and koji.is_debuginfo(rpm['name']):

-                 continue

-             if suboptions.key:

-                 fname = pathinfo.signed(rpm, suboptions.key)

-             else:

-                 fname = pathinfo.rpm(rpm)

-             url = pathinfo.build(info) + '/' + fname

-             urls.append((url, os.path.basename(fname)))

- 

-     def _progress(download_t, download_d, upload_t, upload_d):

-         if download_t == 0:

-             percent_done = 0.0

-         else:

-             percent_done = float(download_d)/float(download_t)

-         percent_done_str = "%02d%%" % (percent_done * 100)

-         data_done = _format_size(download_d)

- 

-         sys.stdout.write("[% -36s] % 4s % 10s\r" % ('='*(int(percent_done * 36)), percent_done_str, data_done))

-         sys.stdout.flush()

- 

-     for url, relpath in urls:

-         if '/' in relpath:

-             koji.ensuredir(os.path.dirname(relpath))

-         print(relpath)

-         c = pycurl.Curl()

-         c.setopt(c.URL, url)

-         c.setopt(c.WRITEDATA, open(relpath, 'wb'))

-         if not suboptions.quiet:

-             c.setopt(c.NOPROGRESS, False)

-             c.setopt(c.XFERINFOFUNCTION, _progress)

-         c.perform()

-         print('')

- 

- 

- def anon_handle_download_logs(options, session, args):

-     "[download] Download a logs for package"

- 

-     FAIL_LOG = "task_failed.log"

-     usage = _("usage: %prog download-logs [options] <task-id> [<task-id> ...]")

-     usage += _("\n       %prog download-logs [options] --nvr <n-v-r> [<n-v-r> ...]")

-     usage += _("\n(Specify the --help global option for a list of other help options)")

-     usage += _("\nCreates special log with name %s if task failed." % FAIL_LOG)

-     parser = OptionParser(usage=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 logs from n-v-r"))

-     parser.add_option("-m", "--match", action="append", metavar="PATTERN",

-                       help=_("Get only log matching PATTERN. 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 (xmlrpclib.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)

-         open(full_filename, 'w').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 id: %i" % 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']:

-             sys.stderr.write(_("Warning: 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:

-             sys.stderr.write(_("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)

- 

-     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))

-             assert binfo['task_id'], binfo

-             task_id = binfo['task_id']

-             sys.stdout.write("Using task ID: %s\n" % task_id)

-         else:

-             try:

-                 task_id = int(arg)

-             except ValueError:

-                 error(_("Task id must be number: %r") % task_id)

-                 continue

-         save_logs(task_id, suboptions.match, suboptions.dir, suboptions.recurse)

- 

- 

- def anon_handle_download_task(options, session, args):

-     "[download] Download the output of a build task "

-     usage = _("usage: %prog download-task <task_id>")

-     usage += _("\n(Specify the --help global option for a list of other help options)")

-     parser = OptionParser(usage=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"))

- 

-     (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(",")

- 

-     # get downloadable tasks

- 

-     base_task = session.getTaskInfo(base_task_id)

- 

-     check_downloadable = lambda task: 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(".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))

-                     continue

- 

-             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))

-                     continue

- 

-     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

-     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)

-         print(_("Downloading [%d/%d]: %s") % (number, len(downloads), new_filename))

-         output_file = open(new_filename, "wb")

-         output_file.write(session.downloadTaskOutput(task["id"], filename, volume=volume))

-         output_file.close()

- 

- def anon_handle_wait_repo(options, session, args):

-     "[monitor] Wait for a repo to be regenerated"

-     usage = _("usage: %prog wait-repo [options] <tag>")

-     usage += _("\n(Specify the --help global option for a list of other help options)")

-     parser = OptionParser(usage=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", help=_("Amount of time to wait (in minutes) before giving up (default: 120)"), default=120)

-     parser.add_option("--quiet", action="store_true", help=_("Suppress output, success or failure will be indicated by the return value only"), default=options.quiet)

-     (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]

- 

-     if suboptions.target:

-         target_info = session.getBuildTarget(tag)

-         if not target_info:

-             parser.error("Invalid 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("Invalid tag: %s" % tag)

-         targets = session.getBuildTargets(buildTagID=tag_info['id'])

-         if not targets:

-             print("%(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 = list(maybe.keys())

-                 maybe.sort()

-                 print("Suggested tags: %s" % ', '.join(maybe))

-             return 1

-         tag_id = tag_info['id']

- 

- 

-     for nvr in builds:

-         data = session.getLatestBuilds(tag_id, package=nvr["name"])

-         if len(data) == 0:

-             print("Warning: package %s is not in tag %s" % (nvr["name"], tag))

-         else:

-             present_nvr = [x["nvr"] for x in data][0]

-             if present_nvr != "%s-%s-%s" % (nvr["name"], nvr["version"], nvr["release"]):

-                 print("Warning: nvr %s-%s-%s is not current in tag %s\n  latest build in %s is %s" % (nvr["name"], nvr["version"], nvr["release"], 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:

-                     print("Unsuccessfully waited %s for %s to appear in the %s repo" % (koji.util.duration(start), koji.util.printList(suboptions.builds), tag))

-                 else:

-                     print("Unsuccessfully waited %s for a new %s repo" % (koji.util.duration(start), tag))

-             return 1

- 

-         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

- 

- _search_types = ('package', 'build', 'tag', 'target', 'user', 'host', 'rpm', 'maven', 'win')

- 

- def handle_regen_repo(options, session, args):

-     "[admin] Force a repo to be regenerated"

-     usage = _("usage: %prog regen-repo [options] <tag>")

-     usage += _("\n(Specify the --help global option for a list of other help options)")

-     parser = OptionParser(usage=usage)

-     parser.add_option("--target", action="store_true", help=_("Interpret the argument as a build target name"))

-     parser.add_option("--nowait", action="store_true", 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 the repo"))

-     (suboptions, args) = parser.parse_args(args)

-     if len(args) == 0:

-         parser.error(_("A tag name must be specified"))

-         assert False  # pragma: no cover

-     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"))

-         assert False  # pragma: no cover

-     activate_session(session)

-     tag = args[0]

-     repo_opts = {}

-     if suboptions.target:

-         info = session.getBuildTarget(tag)

-         if not info:

-             parser.error(_("No matching build target: " + tag))

-             assert False  # pragma: no cover

-         tag = info['build_tag_name']

-         info = session.getTag(tag)

-     else:

-         info = session.getTag(tag)

-         if not info:

-             parser.error(_("No matching tag: " + tag))

-             assert False  # pragma: no cover

-         tag = info['name']

-         targets = session.getBuildTargets(buildTagID=info['id'])

-         if not targets:

-             print("Warning: %s is not a build tag" % tag)

-     if not info['arches']:

-         print("Warning: tag %s has an empty arch list" % info['name'])

-     if suboptions.debuginfo:

-         repo_opts['debuginfo'] = True

-     if suboptions.source:

-         repo_opts['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 _running_in_bg() or suboptions.nowait:

-         return

-     else:

-         session.logout()

-         return watch_tasks(session, [task_id], quiet=options.quiet)

- 

- def handle_dist_repo(options, session, args):

-     """Create a yum repo with distribution options"""

-     usage = _("usage: %prog dist-repo [options] tag keyID [keyID...]")

-     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("--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('--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=_('create a dist repository based on a Brew event'))

-     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("--nowait", action='store_true', default=False,

-         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)'))

-     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)

-     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.errpr(_("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(_('unknown tag %s') % tag)

-     if len(task_opts.arch) == 0:

-         arches = taginfo['arches'] or ''

-         task_opts.arch = arches.split()

-         if task_opts.arch == None:

-             parser.error(_('No arches given and no arches associated with tag'))

-     else:

-         for a in task_opts.arch:

-             if not taginfo['arches'] or a not in taginfo['arches']:

-                 print(_('Warning: %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 not 'i686' in task_opts.arch:

-             parser.error(_('The multilib arch (i686) must be included'))

-         if 's390x' in task_opts.arch and not 's390' in task_opts.arch:

-             parser.error(_('The multilib arch (s390) must be included'))

-         if 'ppc64' in task_opts.arch and not 'ppc' 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('')

-     try:

-         task_opts.arch.remove('noarch') # handled specifically

-         task_opts.arch.remove('src') # ditto

-     except ValueError:

-         pass

-     opts = {

-         'arch': task_opts.arch,

-         'comps': task_opts.comps,

-         'delta': old_repos,

-         'event': task_opts.event,

-         'inherit': not task_opts.noinherit,

-         'latest': task_opts.latest,

-         'multilib': task_opts.multilib,

-         'skip_missing_signatures': task_opts.skip_missing_signatures,

-         'allow_missing_signatures': task_opts.allow_missing_signatures

-     }

-     task_id = session.distRepo(tag, keys, **opts)

-     print("Creating dist repo for tag " + tag)

-     if _running_in_bg() or task_opts.nowait:

-         return

-     else:

-         session.logout()

-         return watch_tasks(session, [task_id], quiet=options.quiet)

- 

- 

- def anon_handle_search(options, session, args):

-     "[search] Search the system"

-     usage = _("usage: %prog search [options] search_type pattern")

-     usage += _('\nAvailable search types: %s') % ', '.join(_search_types)

-     usage += _("\n(Specify the --help global option for a list of other help options)")

-     parser = OptionParser(usage=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 not args:

-         parser.print_help()

-         return

-     type = args[0]

-     if type not in _search_types:

-         parser.error(_("Unknown search type: %s") % type)

-     pattern = args[1]

-     matchType = 'glob'

-     if options.regex:

-         matchType = 'regexp'

-     elif options.exact:

-         matchType = 'exact'

-     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=usage)

-     (opts, args) = parser.parse_args(args)

-     if len(args) != 0:

-         parser.error(_("This command takes no arguments"))

-         assert False  # pragma: no cover

-     activate_session(session)

-     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" % u["krb_principal"])

-     elif authtype == koji.AUTHTYPE_SSL:

-         print("Authenticated via client certificate %s" % options.cert)

- 

- def handle_runroot(options, session, args):

-     "[admin] Run a command in a buildroot"

-     usage = _("usage: %prog runroot [options] <tag> <arch> <command>")

-     usage += _("\n(Specify the --help global option for a list of other help options)")

-     parser = OptionParser(usage=usage)

-     parser.disable_interspersed_args()

-     parser.add_option("-p", "--package", action="append", default=[], help=_("make sure this package is in the chroot"))

-     parser.add_option("-m", "--mount", action="append", default=[], help=_("mount this directory read-write in the chroot"))

-     parser.add_option("--skip-setarch", action="store_true", default=False,

-             help=_("bypass normal setarch in the chroot"))

-     parser.add_option("-w", "--weight", type='int', help=_("set task weight"))

-     parser.add_option("--channel-override", help=_("use a non-standard channel"))

-     parser.add_option("--task-id", action="store_true", default=False,

-             help=_("Print the ID of the runroot task"))

-     parser.add_option("--use-shell", action="store_true", default=False,

-             help=_("Run command through a shell, otherwise uses exec"))

-     parser.add_option("--new-chroot", action="store_true", default=False,

-             help=_("Run command with the --new-chroot (systemd-nspawn) option to mock"))

-     parser.add_option("--repo-id", type="int", help=_("ID of the repo to use"))

- 

-     (opts, args) = parser.parse_args(args)

- 

-     if len(args) < 3:

-         parser.error(_("Incorrect number of arguments"))

-     activate_session(session)

-     tag = args[0]

-     arch = args[1]

-     if opts.use_shell:

-         # everything must be correctly quoted

-         command = ' '.join(args[2:])

-     else:

-         command = args[2:]

-     try:

-         kwargs = { 'channel':       opts.channel_override,

-                    'packages':      opts.package,

-                    'mounts':        opts.mount,

-                    'repo_id':       opts.repo_id,

-                    'skip_setarch':  opts.skip_setarch,

-                    'weight':        opts.weight }

-         # Only pass this kwarg if it is true - this prevents confusing older

-         # builders with a different function signature

-         if opts.new_chroot:

-             kwargs['new_chroot'] = True

- 

-         task_id = session.runroot(tag, arch, command, **kwargs)

-     except koji.GenericError as e:

-         if 'Invalid method' in str(e):

-             print("* The runroot plugin appears to not be installed on the"

-                   " koji hub.  Please contact the administrator.")

-         raise

-     if opts.task_id:

-         print(task_id)

- 

-     try:

-         while True:

-             # wait for the task to finish

-             if session.taskFinished(task_id):

-                 break

-             time.sleep(options.poll_interval)

-     except KeyboardInterrupt:

-         # this is probably the right thing to do here

-         print("User interrupt: canceling runroot task")

-         session.cancelTask(task_id)

-         raise

-     output = list_task_output_all_volumes(session, task_id)

-     if 'runroot.log' in output:

-         for volume in output['runroot.log']:

-             log = session.downloadTaskOutput(task_id, 'runroot.log', volume=volume)

-             sys.stdout.write(log)

-     info = session.getTaskInfo(task_id)

-     if info is None:

-         sys.exit(1)

-     state = koji.TASK_STATES[info['state']]

-     if state in ('FAILED', 'CANCELED'):

-         sys.exit(1)

-     return

- 

- 

- def handle_save_failed_tree(options, session, args):

-     "Create tarball with whole buildtree"

-     usage = _("usage: %prog save-failed-tree [options] ID")

-     usage += _("\n(Specify the --help global option for a list of other help options)")

-     parser = OptionParser(usage=usage)

-     parser.add_option("-f", "--full", action="store_true", default=False,

-             help=_("Download whole tree, if not specified, only builddir will be downloaded"))

-     parser.add_option("-t", "--task", action="store_const", dest="mode",

-             const="task", default="task",

-             help=_("Treat ID as a task ID (the default)"))

-     parser.add_option("-r", "--buildroot", action="store_const", dest="mode",

-             const="buildroot",

-             help=_("Treat ID as a buildroot ID"))

-     parser.add_option("--quiet", action="store_true", default=options.quiet,

-                       help=_("Do not print the task information"))

-     parser.add_option("--nowait", action="store_true",

-                       help=_("Don't wait on build"))

- 

-     (opts, args) = parser.parse_args(args)

- 

-     if len(args) != 1:

-         parser.error(_("List exactly one task or buildroot ID"))

- 

-     try:

-         id_val = int(args[0])

-     except ValueError:

-         parser.error(_("ID must be an integer"))

- 

-     activate_session(session)

- 

-     if opts.mode == "buildroot":

-         br_id = id_val

-     else:

-         brs = [b['id'] for b in session.listBuildroots(taskID=id_val)]

-         if not brs:

-             print(_("No buildroots for task %s") % id_val)

-             return 1

-         br_id = max(brs)

-         if len(brs) > 1:

-             print(_("Multiple buildroots for task. Choosing last one (%s)") % br_id)

- 

-     try:

-         task_id = session.saveFailedTree(br_id, opts.full)

-     except koji.GenericError as e:

-         m = str(e)

-         if 'Invalid method' in m:

-             print(_("* The save_failed_tree plugin appears to not be "

-                     "installed on the koji hub.  Please contact the "

-                     "administrator."))

-             return 1

-         raise

- 

-     if not opts.quiet:

-         print(_("Created task %s for buildroot %s") % (task_id, br_id))

-         print("Task info: %s/taskinfo?taskID=%s"

-                 % (options.weburl, task_id))

- 

-     if opts.nowait:

-         return

-     else:

-         session.logout()

-         watch_tasks(session, [task_id], quiet=opts.quiet)

- 

- 

- def handle_help(options, session, args):

-     "[info] List available commands"

-     usage = _("usage: %prog help <category> ...")

-     usage += _("\n(Specify the --help global option for a list of other help options)")

-     parser = OptionParser(usage=usage)

-     # the --admin opt is for backwards compatibility. It is equivalent to: koji help admin

-     parser.add_option("--admin", action="store_true", help=optparse.SUPPRESS_HELP)

- 

-     (options, args) = parser.parse_args(args)

- 

-     chosen = set(args)

-     if options.admin:

-         chosen.add('admin')

-     avail = set(list(categories.keys()) + ['all'])

-     unavail = chosen - avail

-     for arg in unavail:

-         print("No such help category: %s" % arg)

- 

-     if not chosen:

-         list_commands()

-     else:

-         list_commands(chosen)

- 

- 

- def list_commands(categories_chosen=None):

-     if categories_chosen is None or "all" in categories_chosen:

-         categories_chosen = list(categories.keys())

-     else:

-         # copy list since we're about to modify it

-         categories_chosen = list(categories_chosen)

-     categories_chosen.sort()

-     handlers = []

-     for name,value in globals().items():

-         if name.startswith('handle_'):

-             alias = name.replace('handle_','')

-             alias = alias.replace('_','-')

-             handlers.append((alias,value))

-         elif name.startswith('anon_handle_'):

-             alias = name.replace('anon_handle_','')

-             alias = alias.replace('_','-')

-             handlers.append((alias,value))

-     handlers.sort()

-     print(_("Available commands:"))

-     for category in categories_chosen:

-         print(_("\n%s:" % categories[category]))

-         for alias,handler in handlers:

-             desc = handler.__doc__

-             if desc.startswith('[%s] ' % category):

-                 desc = desc[len('[%s] ' % category):]

-             elif category != 'misc' or desc.startswith('['):

-                 continue

-             print("        %-25s %s" % (alias, desc))

- 

-     print("%s" % get_epilog_str().rstrip("\n"))

- 

- def error(msg=None, code=1):

-     if msg:

-         sys.stderr.write(msg + "\n")

-         sys.stderr.flush()

-     sys.exit(code)

- 

- def warn(msg):

-     sys.stderr.write(msg + "\n")

-     sys.stderr.flush()

- 

- def has_krb_creds():

-     if krbV is None:

-         return False

-     try:

-         ctx = krbV.default_context()

-         ccache = ctx.default_ccache()

-         princ = ccache.principal()

-         return True

-     except krbV.Krb5Error:

-         return False

- 

- def activate_session(session):

-     """Test and login the session is applicable"""

-     global options

-     if options.authtype == "noauth" or options.noauth:

-         #skip authentication

-         pass

-     elif options.authtype == "ssl" or os.path.isfile(options.cert) and options.authtype is None:

-         # authenticate using SSL client cert

-         session.ssl_login(options.cert, None, options.serverca, proxyuser=options.runas)

-     elif options.authtype == "password" or options.user and options.authtype is None:

-         # authenticate using user/password

-         session.login()

-     elif options.authtype == "kerberos" or has_krb_creds() and options.authtype is None:

-         try:

-             if options.keytab and options.principal:

-                 session.krb_login(principal=options.principal, keytab=options.keytab, proxyuser=options.runas)

-             else:

-                 session.krb_login(proxyuser=options.runas)

-         except socket.error as e:

-             warn(_("Could not connect to Kerberos authentication service: %s") % e.args[1])

-         except Exception as e:

-             if krbV is not None and isinstance(e, krbV.Krb5Error):

-                 error(_("Kerberos authentication failed: %s (%s)") % (e.args[1], e.args[0]))

-             else:

-                 raise

-     if not options.noauth and options.authtype != "noauth" and not session.logged_in:

-         error(_("Unable to log in, no authentication methods available"))

-     ensure_connection(session)

-     if options.debug:

-         print("successfully connected to hub")

  

  if __name__ == "__main__":

      global options

file modified
+3
@@ -32,3 +32,6 @@ 

  

  ;certificate of the CA that issued the HTTP server certificate

  ;serverca = ~/.koji/serverca.crt

+ 

+ ;enabled plugins for CLI, runroot and save_failed_tree are available

+ ;plugins =

@@ -0,0 +1,28 @@ 

+ PYFILES = $(wildcard *.py)

+ 

+ PYTHON=python

+ PACKAGE = $(shell basename `pwd`)

+ PYVER := $(shell $(PYTHON) -c 'import sys; print("%.3s" % (sys.version))')

+ PYSYSDIR := $(shell $(PYTHON) -c 'import sys; print(sys.prefix)')

+ PYLIBDIR = $(PYSYSDIR)/lib/python$(PYVER)

+ PKGDIR = $(PYLIBDIR)/site-packages/$(PACKAGE)

+ 

+ _default:

+ 	@echo "nothing to make.  try make install"

+ 

+ clean:

+ 	rm -f *.o *.so *.pyc *~

+ 

+ install:

+ 	@if [ "$(DESTDIR)" = "" ]; then \

+ 		echo " "; \

+ 		echo "ERROR: A destdir is required"; \

+ 		exit 1; \

+ 	fi

+ 

+ 	mkdir -p $(DESTDIR)/$(PKGDIR)

+ 	for p in $(PYFILES) ; do \

+ 		install -p -m 644 $$p $(DESTDIR)/$(PKGDIR)/$$p; \

+ 	done

+ 

+ 	$(PYTHON) -c "import compileall; compileall.compile_dir('$(DESTDIR)/$(PKGDIR)', 1, '$(PYDIR)', 1)"

empty or binary file added
The added file is too large to be shown here, see it at: cli/koji_cli/commands.py
file added
+525
@@ -0,0 +1,525 @@ 

+ # coding=utf-8

+ from __future__ import absolute_import

+ from __future__ import division

+ import optparse

+ import os

+ import random

+ import six

+ import socket

+ import string

+ import sys

+ import time

+ from six.moves import range

+ 

+ try:

+     import krbV

+ except ImportError:  # pragma: no cover

+     krbV = None

+ 

+ import koji

+ 

+ # fix OptionParser for python 2.3 (optparse verion 1.4.1+)

+ # code taken from optparse version 1.5a2

+ OptionParser = optparse.OptionParser

+ if optparse.__version__ == "1.4.1+":  # pragma: no cover

+     def _op_error(self, msg):

+         self.print_usage(sys.stderr)

+         msg = "%s: error: %s\n" % (self._get_prog_name(), msg)

+         if msg:

+             sys.stderr.write(msg)

+         sys.exit(2)

+     OptionParser.error = _op_error

+ 

+ greetings = ('hello', 'hi', 'yo', "what's up", "g'day", 'back to work',

+              'bonjour',

+              'hallo',

+              'ciao',

+              'hola',

+             u'olá',

+             u'dobrý den',

+             u'zdravstvuite',

+             u'góðan daginn',

+              'hej',

+              'tervehdys',

+             u'grüezi',

+             u'céad míle fáilte',

+             u'hylô',

+             u'bună ziua',

+             u'jó napot',

+              'dobre dan',

+             u'你好',

+             u'こんにちは',

+             u'नमस्कार',

+             u'안녕하세요')

+ 

+ ARGMAP = {'None': None,

+           'True': True,

+           'False': False}

+ 

+ 

+ def _(args):

+     """Stub function for translation"""

+     return args

+ 

+ 

+ def arg_filter(arg):

+     try:

+         return int(arg)

+     except ValueError:

+         pass

+     try:

+         return float(arg)

+     except ValueError:

+         pass

+     if arg in ARGMAP:

+         return ARGMAP[arg]

+     #handle lists/dicts?

+     return arg

+ 

+ 

+ categories = {

+     'admin' : 'admin commands',

+     'build' : 'build commands',

+     'search' : 'search commands',

+     'download' : 'download commands',

+     'monitor'  : 'monitor commands',

+     'info' : 'info commands',

+     'bind' : 'bind commands',

+     'misc' : 'miscellaneous commands',

+ }

+ 

+ 

+ def get_epilog_str(progname=None):

+     if progname is None:

+         progname = os.path.basename(sys.argv[0]) or 'koji'

+     categories_ordered=', '.join(sorted(['all'] + list(categories.keys())))

+     epilog_str = '''

+ Try "%(progname)s --help" for help about global options

+ Try "%(progname)s help" to get all available commands

+ Try "%(progname)s <command> --help" for help about the options of a particular command

+ Try "%(progname)s help <category>" to get commands under a particular category

+ Available categories are: %(categories)s

+ ''' % ({'progname': progname, 'categories': categories_ordered})

+     return _(epilog_str)

+ 

+ 

+ def ensure_connection(session):

+     try:

+         ret = session.getAPIVersion()

+     except six.moves.xmlrpc_client.ProtocolError:

+         error(_("Error: Unable to connect to server"))

+     if ret != koji.API_VERSION:

+         warn(_("WARNING: The server is at API version %d and the client is at %d" % (ret, koji.API_VERSION)))

+ 

+ 

+ def print_task_headers():

+     """Print the column headers"""

+     print("ID       Pri  Owner                State    Arch       Name")

+ 

+ 

+ def print_task(task,depth=0):

+     """Print a task"""

+     task = task.copy()

+     task['state'] = koji.TASK_STATES.get(task['state'],'BADSTATE')

+     fmt = "%(id)-8s %(priority)-4s %(owner_name)-20s %(state)-8s %(arch)-10s "

+     if depth:

+         indent = "  "*(depth-1) + " +"

+     else:

+         indent = ''

+     label = koji.taskLabel(task)

+     print(''.join([fmt % task, indent, label]))

+ 

+ 

+ def print_task_recurse(task,depth=0):

+     """Print a task and its children"""

+     print_task(task,depth)

+     for child in task.get('children',()):

+         print_task_recurse(child,depth+1)

+ 

+ 

+ def parse_arches(arches, to_list=False):

+     """Parse comma or space-separated list of arches and return

+        only space-separated one."""

+     arches = arches.replace(',', ' ').split()

+     if to_list:

+         return arches

+     else:

+         return ' '.join(arches)

+ 

+ 

+ class TaskWatcher(object):

+ 

+     def __init__(self,task_id,session,level=0,quiet=False):

+         self.id = task_id

+         self.session = session

+         self.info = None

+         self.level = level

+         self.quiet = quiet

+ 

+     #XXX - a bunch of this stuff needs to adapt to different tasks

+ 

+     def str(self):

+         if self.info:

+             label = koji.taskLabel(self.info)

+             return "%s%d %s" % ('  ' * self.level, self.id, label)

+         else:

+             return "%s%d" % ('  ' * self.level, self.id)

+ 

+     def __str__(self):

+         return self.str()

+ 

+     def get_failure(self):

+         """Print infomation about task completion"""

+         if self.info['state'] != koji.TASK_STATES['FAILED']:

+             return ''

+         error = None

+         try:

+             self.session.getTaskResult(self.id)

+         except (six.moves.xmlrpc_client.Fault,koji.GenericError) as e:

+             error = e

+         if error is None:

+             # print("%s: complete" % self.str())

+             # We already reported this task as complete in update()

+             return ''

+         else:

+             return '%s: %s' % (error.__class__.__name__, str(error).strip())

+ 

+     def update(self):

+         """Update info and log if needed.  Returns True on state change."""

+         if self.is_done():

+             # Already done, nothing else to report

+             return False

+         last = self.info

+         self.info = self.session.getTaskInfo(self.id, request=True)

+         if self.info is None:

+             if not self.quiet:

+                 print("No such task id: %i" % self.id)

+             sys.exit(1)

+         state = self.info['state']

+         if last:

+             #compare and note status changes

+             laststate = last['state']

+             if laststate != state:

+                 if not self.quiet:

+                     print("%s: %s -> %s" % (self.str(), self.display_state(last), self.display_state(self.info)))

+                 return True

+             return False

+         else:

+             # First time we're seeing this task, so just show the current state

+             if not self.quiet:

+                 print("%s: %s" % (self.str(), self.display_state(self.info)))

+             return False

+ 

+     def is_done(self):

+         if self.info is None:

+             return False

+         state = koji.TASK_STATES[self.info['state']]

+         return (state in ['CLOSED','CANCELED','FAILED'])

+ 

+     def is_success(self):

+         if self.info is None:

+             return False

+         state = koji.TASK_STATES[self.info['state']]

+         return (state == 'CLOSED')

+ 

+     def display_state(self, info):

+         # We can sometimes be passed a task that is not yet open, but

+         # not finished either.  info would be none.

+         if not info:

+             return 'unknown'

+         if info['state'] == koji.TASK_STATES['OPEN']:

+             if info['host_id']:

+                 host = self.session.getHost(info['host_id'])

+                 return 'open (%s)' % host['name']

+             else:

+                 return 'open'

+         elif info['state'] == koji.TASK_STATES['FAILED']:

+             return 'FAILED: %s' % self.get_failure()

+         else:

+             return koji.TASK_STATES[info['state']].lower()

+ 

+ 

+ def display_tasklist_status(tasks):

+     free = 0

+     open = 0

+     failed = 0

+     done = 0

+     for task_id in tasks.keys():

+         status = tasks[task_id].info['state']

+         if status == koji.TASK_STATES['FAILED']:

+             failed += 1

+         elif status == koji.TASK_STATES['CLOSED'] or status == koji.TASK_STATES['CANCELED']:

+             done += 1

+         elif status == koji.TASK_STATES['OPEN'] or status == koji.TASK_STATES['ASSIGNED']:

+             open += 1

+         elif status == koji.TASK_STATES['FREE']:

+             free += 1

+     print("  %d free  %d open  %d done  %d failed" % (free, open, done, failed))

+ 

+ 

+ def display_task_results(tasks):

+     for task in [task for task in tasks.values() if task.level == 0]:

+         state = task.info['state']

+         task_label = task.str()

+ 

+         if state == koji.TASK_STATES['CLOSED']:

+             print('%s completed successfully' % task_label)

+         elif state == koji.TASK_STATES['FAILED']:

+             print('%s failed' % task_label)

+         elif state == koji.TASK_STATES['CANCELED']:

+             print('%s was canceled' % task_label)

+         else:

+             # shouldn't happen

+             print('%s has not completed' % task_label)

+ 

+ 

+ def watch_tasks(session, tasklist, quiet=False, poll_interval=60):

+     if not tasklist:

+         return

+     if not quiet:

+         print("Watching tasks (this may be safely interrupted)...")

+     sys.stdout.flush()

+     rv = 0

+     try:

+         tasks = {}

+         for task_id in tasklist:

+             tasks[task_id] = TaskWatcher(task_id,session,quiet=quiet)

+         while True:

+             all_done = True

+             for task_id, task in list(tasks.items()):

+                 changed = task.update()

+                 if not task.is_done():

+                     all_done = False

+                 else:

+                     if changed:

+                         # task is done and state just changed

+                         if not quiet:

+                             display_tasklist_status(tasks)

+                     if not task.is_success():

+                         rv = 1

+                 for child in session.getTaskChildren(task_id):

+                     child_id = child['id']

+                     if not child_id in list(tasks.keys()):

+                         tasks[child_id] = TaskWatcher(child_id, session, task.level + 1, quiet=quiet)

+                         tasks[child_id].update()

+                         # If we found new children, go through the list again,

+                         # in case they have children also

+                         all_done = False

+             if all_done:

+                 if not quiet:

+                     print('')

+                     display_task_results(tasks)

+                 break

+ 

+             sys.stdout.flush()

+             time.sleep(poll_interval)

+     except KeyboardInterrupt:

+         if tasks and not quiet:

+             progname = os.path.basename(sys.argv[0]) or 'koji'

+             tlist = ['%s: %s' % (t.str(), t.display_state(t.info))

+                             for t in tasks.values() if not t.is_done()]

+             print( \

+ """Tasks still running. You can continue to watch with the '%s watch-task' command.

+ Running Tasks:

+ %s""" % (progname, '\n'.join(tlist)))

+         raise

+     return rv

+ 

+ 

+ def watch_logs(session, tasklist, opts, poll_interval):

+     print("Watching logs (this may be safely interrupted)...")

+ 

+     def _isDone(session, taskId):

+         info = session.getTaskInfo(taskId)

+         if info is None:

+             print("No such task id: %i" % taskId)

+             sys.exit(1)

+         state = koji.TASK_STATES[info['state']]

+         return (state in ['CLOSED','CANCELED','FAILED'])

+ 

+     offsets = {}

+     for task_id in tasklist:

+         offsets[task_id] = {}

+ 

+     lastlog = None

+     while True:

+         for task_id in tasklist[:]:

+             if _isDone(session, task_id):

+                 tasklist.remove(task_id)

+ 

+             output = list_task_output_all_volumes(session, task_id)

+             # convert to list of (file, volume)

+             files = []

+             for filename, volumes in six.iteritems(output):

+                 files += [(filename, volume) for volume in volumes]

+ 

+             if opts.log:

+                 logs = [file_volume for file_volume in files if file_volume[0] == opts.log]

+             else:

+                 logs = [file_volume for file_volume in files if file_volume[0].endswith('log')]

+ 

+             taskoffsets = offsets[task_id]

+             for log, volume in logs:

+                 contents = 'placeholder'

+                 while contents:

+                     if (log, volume) not in taskoffsets:

+                         taskoffsets[(log, volume)] = 0

+ 

+                     contents = session.downloadTaskOutput(task_id, log, taskoffsets[(log, volume)], 16384, volume=volume)

+                     taskoffsets[(log, volume)] += len(contents)

+                     if contents:

+                         currlog = "%d:%s:%s:" % (task_id, volume, log)

+                         if currlog != lastlog:

+                             if lastlog:

+                                 sys.stdout.write("\n")

+                             sys.stdout.write("==> %s <==\n" % currlog)

+                             lastlog = currlog

+                         sys.stdout.write(contents.decode('utf8'))

+ 

+         if not tasklist:

+             break

+ 

+         time.sleep(poll_interval)

+ 

+ 

+ def list_task_output_all_volumes(session, task_id):

+     """List task output with all volumes, or fake it"""

+     try:

+         return session.listTaskOutput(task_id, all_volumes=True)

+     except koji.GenericError as e:

+         if 'got an unexpected keyword argument' not in str(e):

+             raise

+     # otherwise leave off the option and fake it

+     output = session.listTaskOutput(task_id)

+     return dict([fn, ['DEFAULT']] for fn in output)

+ 

+ 

+ def _unique_path(prefix):

+     """Create a unique path fragment by appending a path component

+     to prefix.  The path component will consist of a string of letter and numbers

+     that is unlikely to be a duplicate, but is not guaranteed to be unique."""

+     # Use time() in the dirname to provide a little more information when

+     # browsing the filesystem.

+     # For some reason repr(time.time()) includes 4 or 5

+     # more digits of precision than str(time.time())

+     return '%s/%r.%s' % (prefix, time.time(),

+                       ''.join([random.choice(string.ascii_letters) for i in range(8)]))

+ 

+ 

+ def _format_size(size):

+     if (size / 1073741824 >= 1):

+         return "%0.2f GiB" % (size / 1073741824.0)

+     if (size / 1048576 >= 1):

+         return "%0.2f MiB" % (size / 1048576.0)

+     if (size / 1024 >=1):

+         return "%0.2f KiB" % (size / 1024.0)

+     return "%0.2f B" % (size)

+ 

+ 

+ def _format_secs(t):

+     h = t / 3600

+     t %= 3600

+     m = t / 60

+     s = t % 60

+     return "%02d:%02d:%02d" % (h, m, s)

+ 

+ 

+ def _progress_callback(uploaded, total, piece, time, total_time):

+     if total == 0:

+         percent_done = 0.0

+     else:

+         percent_done = float(uploaded)/float(total)

+     percent_done_str = "%02d%%" % (percent_done * 100)

+     data_done = _format_size(uploaded)

+     elapsed = _format_secs(total_time)

+ 

+     speed = "- B/sec"

+     if (time):

+         if (uploaded != total):

+             speed = _format_size(float(piece)/float(time)) + "/sec"

+         else:

+             speed = _format_size(float(total)/float(total_time)) + "/sec"

+ 

+     # write formated string and flush

+     sys.stdout.write("[% -36s] % 4s % 8s % 10s % 14s\r" % ('='*(int(percent_done*36)), percent_done_str, elapsed, data_done, speed))

+     sys.stdout.flush()

+ 

+ 

+ def _running_in_bg():

+     try:

+         return (not os.isatty(0)) or (os.getpgrp() != os.tcgetpgrp(0))

+     except OSError:

+         return True

+ 

+ 

+ def linked_upload(localfile, path, name=None):

+     """Link a file into the (locally writable) workdir, bypassing upload"""

+     old_umask = os.umask(0o02)

+     try:

+         if name is None:

+             name = os.path.basename(localfile)

+         dest_dir = os.path.join(koji.pathinfo.work(), path)

+         dst = os.path.join(dest_dir, name)

+         koji.ensuredir(dest_dir)

+         # fix uid/gid to keep httpd happy

+         st = os.stat(koji.pathinfo.work())

+         os.chown(dest_dir, st.st_uid, st.st_gid)

+         print("Linking rpm to: %s" % dst)

+         os.link(localfile, dst)

+     finally:

+         os.umask(old_umask)

+ 

+ 

+ def error(msg=None, code=1):

+     if msg:

+         sys.stderr.write(msg + "\n")

+         sys.stderr.flush()

+     sys.exit(code)

+ 

+ 

+ def warn(msg):

+     sys.stderr.write(msg + "\n")

+     sys.stderr.flush()

+ 

+ 

+ def has_krb_creds():

+     if krbV is None:

+         return False

+     try:

+         ctx = krbV.default_context()

+         ccache = ctx.default_ccache()

+         ccache.principal()

+         return True

+     except krbV.Krb5Error:

+         return False

+ 

+ 

+ def activate_session(session, options):

+     """Test and login the session is applicable"""

+     if options.authtype == "noauth" or options.noauth:

+         #skip authentication

+         pass

+     elif options.authtype == "ssl" or os.path.isfile(options.cert) and options.authtype is None:

+         # authenticate using SSL client cert

+         session.ssl_login(options.cert, None, options.serverca, proxyuser=options.runas)

+     elif options.authtype == "password" or options.user and options.authtype is None:

+         # authenticate using user/password

+         session.login()

+     elif options.authtype == "kerberos" or has_krb_creds() and options.authtype is None:

+         try:

+             if options.keytab and options.principal:

+                 session.krb_login(principal=options.principal, keytab=options.keytab, proxyuser=options.runas)

+             else:

+                 session.krb_login(proxyuser=options.runas)

+         except socket.error as e:

+             warn(_("Could not connect to Kerberos authentication service: %s") % e.args[1])

+         except Exception as e:

+             if krbV is not None and isinstance(e, krbV.Krb5Error):

+                 error(_("Kerberos authentication failed: %s (%s)") % (e.args[1], e.args[0]))

+             else:

+                 raise

+     if not options.noauth and options.authtype != "noauth" and not session.logged_in:

+         error(_("Unable to log in, no authentication methods available"))

+     ensure_connection(session)

+     if options.debug:

+         print("successfully connected to hub")

@@ -188,3 +188,63 @@ 

  ::

  

      $ koji tag-build mytag mypkg-1.0-1

+ 

+ New command for CLI

+ -------------------

+ 

+ When you add new XMLRPC call or just wanted to do some more complicated

+ things with API, you can benefit from writing a new command for CLI.

+ 

+ Most simple command would look like this:

+ 

+ ::

+ 

+     from koji.plugin import export_cli

+ 

+     @export_cli

+     def anon_handle_echo(options, session, args):

+         usage = _("usage: %prog echo <message>")

+         parser = OptionParser(usage=usage)

+         (opts, args) = parser.parse_args(args)

+         print(args[0])

+ 

+ `@export_cli` is decorator which will register plugin's command with name

+ derived from name of the function. Function name needs to follow one rule: It

+ has to start with `anon_handle_` or `handle_`. Rest of the name is name of

+ the command. First one will not authenticate against hub (user can still override

+ this behaviour with `--force-auth` or `--mine` options where it is relevant)

+ - it is simply same as using `--noath` option. Second variant doesn't presume

+   anything about authentication.

+ 

+ Compared to this simple example is real usage with API calls. Koji provides

+ some important functions via client library `koji_cli.lib` which can be

+ imported and used inside command's function. Feel free to examine, what is

+ provided there. Some notable examples are:

+ 

+  * `activate_session(session, options)` - It is needed to authenticate

+    against hub. Both parameters are same as those passed to handler.

+  * `watch_tasks(session, tasklist, quiet=False, poll_interval=60)` - It is

+    the same function used e.g. in `build` command for waiting for spawned

+    tasks.

+  * `list_task_output_all_volumes(session, task_id)` - wrapper function for

+    `listTaskOutput` with different versions of hub.

+ 

+ Final command has to be saved in python system-wide library path - e.g. in

+ `/usr/lib/python3.4/site-packages/koji_cli_plugins`. Filename doesn't matter

+ as all files in this directory are searched for `@export_cli` macros. Note,

+ that python 3 variant of CLI is looking to different directory than python 2

+ one.

+ 

+ CLI plugins structure will be extended (made configurable and allowing more

+ than just adding commands - e.g. own authentication methods, etc.) in future.

+ 

+ Pull requests

+ ^^^^^^^^^^^^^

+ 

+ These plugins have to be written in python 2.6+/3.x compatible way. We are

+ using `six` library to support this, so we will also prefer pull requests

+ written this way. CLI (and client library) is meant to be fully compatible

+ with python 3 from koji 1.13.

+ 

+ Tests are also recommended for PR. For example one see

+ `tests/test_plugins/test_runroot_cli.py`.

file modified
+42
@@ -93,6 +93,26 @@ 

  desc

  %endif

  

+ %package -n python2-%{name}-cli-plugins

+ Summary: Koji client plugins

+ Group: Applications/Internet

+ License: LGPLv2

+ Requires: %{name} = %{version}-%{release}

+ 

+ %description -n python2-%{name}-cli-plugins

+ Plugins to the koji command-line interface

+ 

+ %if 0%{with python3}

+ %package -n python3-%{name}-cli-plugins

+ Summary: Koji client plugins

+ Group: Applications/Internet

+ License: LGPLv2

+ Requires: %{name} = %{version}-%{release}

+ 

+ %description -n python3-%{name}-cli-plugins

+ Plugins to the koji command-line interface

+ %endif

+ 

  %package hub

  Summary: Koji XMLRPC interface

  Group: Applications/Internet
@@ -257,6 +277,10 @@ 

  %if 0%{with python3}

  cd koji

  make DESTDIR=$RPM_BUILD_ROOT PYTHON=python3 %{?install_opt} install

+ cd ../cli

+ make DESTDIR=$RPM_BUILD_ROOT PYTHON=python3 %{?install_opt} install

+ cd ../plugins

+ make DESTDIR=$RPM_BUILD_ROOT PYTHON=python3 %{?install_opt} install

  # alter python interpreter in koji CLI

  sed -i 's/\#\!\/usr\/bin\/python/\#\!\/usr\/bin\/python3/' $RPM_BUILD_ROOT/usr/bin/koji

  %endif
@@ -274,10 +298,28 @@ 

  %files -n python2-%{name}

  %defattr(-,root,root)

  %{python2_sitelib}/%{name}

+ %{python2_sitelib}/koji_cli

  

  %if 0%{with python3}

  %files -n python%{python3_pkgversion}-koji

  %{python3_sitelib}/%{name}

+ %{python3_sitelib}/koji_cli

+ %endif

+ 

+ %files -n python2-%{name}-cli-plugins

+ %defattr(-,root,root)

+ %{python2_sitelib}/koji_cli_plugins

+ # we don't have config files for default plugins yet

+ #%%dir %{_sysconfdir}/koji/plugins

+ #%%config(noreplace) %{_sysconfdir}/koji/plugins/*.conf

+ 

+ %if 0%{with python3}

+ %files -n python%{python3_pkgversion}-%{name}-cli-plugins

+ %defattr(-,root,root)

+ %{python3_sitelib}/koji_cli_plugins

+ # we don't have config files for default plugins yet

+ #%%dir %{_sysconfdir}/koji/plugins

+ #%%config(noreplace) %{_sysconfdir}/koji/plugins/*.conf

  %endif

  

  %files hub

file modified
+1 -1
@@ -2,7 +2,7 @@ 

  PACKAGE = $(shell basename `pwd`)

  ifeq ($(PYTHON), python3)

      # for python3 we fully support only basic library + CLI

-     PYFILES = __init__.py util.py

+     PYFILES = __init__.py util.py plugin.py

      PYSCRIPTS =

      SUBDIRS =

  else

file modified
+2 -1
@@ -2680,7 +2680,8 @@ 

          if volume and volume != 'DEFAULT':

              dlopts['volume'] = volume

          result = self.callMethod('downloadTaskOutput', taskID, fileName, **dlopts)

-         return base64.decodestring(result)

+         return base64.decodestring(result.encode('ascii'))

+ 

  

  class DBHandler(logging.Handler):

      """

file modified
+9
@@ -105,6 +105,15 @@ 

      setattr(f, 'exported', True)

      return f

  

+ def export_cli(f):

+     """a decorator that marks a function as exported for CLI

+ 

+     intended to be used by plugins

+     the HandlerRegistry will export the function under its own name

+     """

+     setattr(f, 'exported_cli', True)

+     return f

+ 

  def export_as(alias):

      """returns a decorator that marks a function as exported and gives it an alias

  

file modified
+28 -10
@@ -1,10 +1,19 @@ 

  PYTHON=python

+ PYVER := $(shell $(PYTHON) -c 'import sys; print("%.3s" %(sys.version))')

+ PYSYSDIR := $(shell $(PYTHON) -c 'import sys; print(sys.prefix)')

+ PYLIBDIR = $(PYSYSDIR)/lib/python$(PYVER)

+ PKGDIR = $(PYLIBDIR)/site-packages

+ 

+ CLIPLUGINDIR = $(PKGDIR)/koji_cli_plugins

  HUBPLUGINDIR = /usr/lib/koji-hub-plugins

  BUILDERPLUGINDIR = /usr/lib/koji-builder-plugins

+ CLIFILES = $(wildcard cli/*.py)

  HUBFILES = $(wildcard hub/*.py)

  BUILDERFILES = $(wildcard builder/*.py)

+ CLICONFDIR = /etc/koji/plugins

  HUBCONFDIR = /etc/koji-hub/plugins

  BUILDERCONFDIR = /etc/kojid/plugins

+ CLICONFFILES = $(wildcard cli/*.conf)

  HUBCONFFILES = $(wildcard hub/*.conf)

  BUILDERCONFFILES = $(wildcard builder/*.conf)

  
@@ -20,14 +29,23 @@ 

  		echo "ERROR: A destdir is required"; \

  		exit 1; \

  	fi

+ 	if [ "$(PYTHON)" == "python" ] ; then \

+ 		mkdir -p $(DESTDIR)/$(HUBPLUGINDIR); \

+ 		mkdir -p $(DESTDIR)/$(BUILDERPLUGINDIR); \

+ 		install -p -m 644 $(HUBFILES) $(DESTDIR)/$(HUBPLUGINDIR); \

+ 		install -p -m 644 $(BUILDERFILES) $(DESTDIR)/$(BUILDERPLUGINDIR); \

+ 		$(PYTHON) -c "import compileall; compileall.compile_dir('$(DESTDIR)/$(HUBPLUGINDIR)', 1, '$(HUBPLUGINDIR)', 1)"; \

+ 		$(PYTHON) -c "import compileall; compileall.compile_dir('$(DESTDIR)/$(BUILDERPLUGINDIR)', 1, '$(BUILDERPLUGINDIR)', 1)"; \

+ 		mkdir -p $(DESTDIR)/$(HUBCONFDIR); \

+ 		mkdir -p $(DESTDIR)/$(BUILDERCONFDIR); \

+ 		install -p -m 644 $(HUBCONFFILES) $(DESTDIR)/$(HUBCONFDIR); \

+ 		install -p -m 644 $(BUILDERCONFFILES) $(DESTDIR)/$(BUILDERCONFDIR); \

+ 	fi

  

- 	mkdir -p $(DESTDIR)/$(HUBPLUGINDIR)

- 	mkdir -p $(DESTDIR)/$(BUILDERPLUGINDIR)

- 	install -p -m 644 $(HUBFILES) $(DESTDIR)/$(HUBPLUGINDIR)

- 	install -p -m 644 $(BUILDERFILES) $(DESTDIR)/$(BUILDERPLUGINDIR)

- 	$(PYTHON) -c "import compileall; compileall.compile_dir('$(DESTDIR)/$(HUBPLUGINDIR)', 1, '$(HUBPLUGINDIR)', 1)"

- 	$(PYTHON) -c "import compileall; compileall.compile_dir('$(DESTDIR)/$(BUILDERPLUGINDIR)', 1, '$(BUILDERPLUGINDIR)', 1)"

- 	mkdir -p $(DESTDIR)/$(HUBCONFDIR)

- 	mkdir -p $(DESTDIR)/$(BUILDERCONFDIR)

- 	install -p -m 644 $(HUBCONFFILES) $(DESTDIR)/$(HUBCONFDIR)

- 	install -p -m 644 $(BUILDERCONFFILES) $(DESTDIR)/$(BUILDERCONFDIR)

+ 	mkdir -p $(DESTDIR)/$(CLIPLUGINDIR)

+ 	install -p -m 644 $(CLIFILES) $(DESTDIR)/$(CLIPLUGINDIR)

+ 	$(PYTHON) -c "import compileall; compileall.compile_dir('$(DESTDIR)/$(CLIPLUGINDIR)', 1, '$(CLIPLUGINDIR)', 1)"

+ 	mkdir -p $(DESTDIR)/$(CLICONFDIR)

+         ifneq "$(CLICONFFILES)" ""

+ 	install -p -m 644 $(CLICONFFILES) $(DESTDIR)/$(CLICONFDIR)

+         endif

@@ -0,0 +1,84 @@ 

+ import sys

+ import time

+ 

+ import koji

+ from koji.plugin import export_cli

+ from koji_cli.lib import _, activate_session, OptionParser, list_task_output_all_volumes

+ 

+ @export_cli

+ def handle_runroot(options, session, args):

+     "[admin] Run a command in a buildroot"

+     usage = _("usage: %prog runroot [options] <tag> <arch> <command>")

+     usage += _("\n(Specify the --help global option for a list of other help options)")

+     parser = OptionParser(usage=usage)

+     parser.disable_interspersed_args()

+     parser.add_option("-p", "--package", action="append", default=[], help=_("make sure this package is in the chroot"))

+     parser.add_option("-m", "--mount", action="append", default=[], help=_("mount this directory read-write in the chroot"))

+     parser.add_option("--skip-setarch", action="store_true", default=False,

+             help=_("bypass normal setarch in the chroot"))

+     parser.add_option("-w", "--weight", type='int', help=_("set task weight"))

+     parser.add_option("--channel-override", help=_("use a non-standard channel"))

+     parser.add_option("--task-id", action="store_true", default=False,

+             help=_("Print the ID of the runroot task"))

+     parser.add_option("--use-shell", action="store_true", default=False,

+             help=_("Run command through a shell, otherwise uses exec"))

+     parser.add_option("--new-chroot", action="store_true", default=False,

+             help=_("Run command with the --new-chroot (systemd-nspawn) option to mock"))

+     parser.add_option("--repo-id", type="int", help=_("ID of the repo to use"))

+ 

+     (opts, args) = parser.parse_args(args)

+ 

+     if len(args) < 3:

+         parser.error(_("Incorrect number of arguments"))

+     activate_session(session, options)

+     tag = args[0]

+     arch = args[1]

+     if opts.use_shell:

+         # everything must be correctly quoted

+         command = ' '.join(args[2:])

+     else:

+         command = args[2:]

+     try:

+         kwargs = { 'channel':       opts.channel_override,

+                    'packages':      opts.package,

+                    'mounts':        opts.mount,

+                    'repo_id':       opts.repo_id,

+                    'skip_setarch':  opts.skip_setarch,

+                    'weight':        opts.weight }

+         # Only pass this kwarg if it is true - this prevents confusing older

+         # builders with a different function signature

+         if opts.new_chroot:

+             kwargs['new_chroot'] = True

+ 

+         task_id = session.runroot(tag, arch, command, **kwargs)

+     except koji.GenericError as e:

+         if 'Invalid method' in str(e):

+             print("* The runroot plugin appears to not be installed on the"

+                   " koji hub.  Please contact the administrator.")

+         raise

+     if opts.task_id:

+         print(task_id)

+ 

+     try:

+         while True:

+             # wait for the task to finish

+             if session.taskFinished(task_id):

+                 break

+             time.sleep(options.poll_interval)

+     except KeyboardInterrupt:

+         # this is probably the right thing to do here

+         print("User interrupt: canceling runroot task")

+         session.cancelTask(task_id)

+         raise

+     output = list_task_output_all_volumes(session, task_id)

+     if 'runroot.log' in output:

+         for volume in output['runroot.log']:

+             log = session.downloadTaskOutput(task_id, 'runroot.log', volume=volume)

+             sys.stdout.write(log)

+     info = session.getTaskInfo(task_id)

+     if info is None:

+         sys.exit(1)

+     state = koji.TASK_STATES[info['state']]

+     if state in ('FAILED', 'CANCELED'):

+         sys.exit(1)

+     return

@@ -0,0 +1,67 @@ 

+ import koji

+ from koji.plugin import export_cli

+ from koji_cli.lib import _, activate_session, OptionParser, watch_tasks

+ 

+ @export_cli

+ def handle_save_failed_tree(options, session, args):

+     "Create tarball with whole buildtree"

+     usage = _("usage: %prog save-failed-tree [options] ID")

+     usage += _("\n(Specify the --help global option for a list of other help options)")

+     parser = OptionParser(usage=usage)

+     parser.add_option("-f", "--full", action="store_true", default=False,

+             help=_("Download whole tree, if not specified, only builddir will be downloaded"))

+     parser.add_option("-t", "--task", action="store_const", dest="mode",

+             const="task", default="task",

+             help=_("Treat ID as a task ID (the default)"))

+     parser.add_option("-r", "--buildroot", action="store_const", dest="mode",

+             const="buildroot",

+             help=_("Treat ID as a buildroot ID"))

+     parser.add_option("--quiet", action="store_true", default=options.quiet,

+                       help=_("Do not print the task information"))

+     parser.add_option("--nowait", action="store_true",

+                       help=_("Don't wait on build"))

+ 

+     (opts, args) = parser.parse_args(args)

+ 

+     if len(args) != 1:

+         parser.error(_("List exactly one task or buildroot ID"))

+ 

+     try:

+         id_val = int(args[0])

+     except ValueError:

+         parser.error(_("ID must be an integer"))

+ 

+     activate_session(session, options)

+ 

+     if opts.mode == "buildroot":

+         br_id = id_val

+     else:

+         brs = [b['id'] for b in session.listBuildroots(taskID=id_val)]

+         if not brs:

+             print(_("No buildroots for task %s") % id_val)

+             return 1

+         br_id = max(brs)

+         if len(brs) > 1:

+             print(_("Multiple buildroots for task. Choosing last one (%s)") % br_id)

+ 

+     try:

+         task_id = session.saveFailedTree(br_id, opts.full)

+     except koji.GenericError as e:

+         m = str(e)

+         if 'Invalid method' in m:

+             print(_("* The save_failed_tree plugin appears to not be "

+                     "installed on the koji hub.  Please contact the "

+                     "administrator."))

+             return 1

+         raise

+ 

+     if not opts.quiet:

+         print(_("Created task %s for buildroot %s") % (task_id, br_id))

+         print("Task info: %s/taskinfo?taskID=%s"

+                 % (options.weburl, task_id))

+ 

+     if opts.nowait:

+         return

+     else:

+         session.logout()

+         watch_tasks(session, [task_id], quiet=opts.quiet, poll_interval=options.poll_interval)

file added
+104
@@ -0,0 +1,104 @@ 

+ #!/usr/bin/python

+ 

+ import imp

+ import inspect

+ import koji

+ import os.path

+ import sys

+ 

+ filename = sys.argv[1]

+ fo = file(filename)

+ mod = imp.load_module('some_code', fo, fo.name, ('.py', 'U', 1))

+ fo.close()

+ 

+ destdir = sys.argv[2]

+ if not os.path.isdir(destdir):

+     raise Exception("Not a directory" % destdir)

+ 

+ def get_dest(name, obj):

+     dest = None

+     if name in ['get_options', 'handle_help', 'list_commands']:

+         dest = 'cli/koji'

+     elif name in ['handle_runroot', 'handle_save_failed_tree']:

+         dest = 'plugins/cli/' + name[7:] + '.py'

+     elif name.startswith('handle_') or name.startswith('anon_handle'):

+         dest = 'cli/koji_cli/commands.py'

+     elif inspect.isclass(obj):

+         dest = 'cli/koji_cli/lib.py'

+     elif name.startswith('print_group_'):

+         dest = 'cli/koji_cli/commands.py'

+     elif name.startswith('_import_comps'):

+         dest = 'cli/koji_cli/commands.py'

+     elif not name.startswith('_'):

+         dest = 'cli/koji_cli/lib.py'

+     elif name in ['_unique_path', '_format_size', '_format_secs',

+             '_progress_callback', '_running_in_bg', '_']:

+         dest = 'cli/koji_cli/lib.py'

+     elif name.startswith('_'):

+         dest = 'cli/koji_cli/commands.py'

+     return dest

+ 

+ order = []

+ modfile = inspect.getsourcefile(mod)

+ sys.stderr.write("Module file: %r\n" % modfile)

+ for name in vars(mod):

+     obj = getattr(mod, name)

+     if inspect.isclass(obj) or inspect.isfunction(obj):

+         try:

+             objfile = inspect.getsourcefile(obj)

+         except TypeError as ex:

+             sys.stderr.write("Skipping %s from %s\n" % (name, obj))

+             continue

+         if objfile != modfile:

+             sys.stderr.write("Skipping %s from %s\n" % (name, inspect.getfile(obj)))

+             continue

+         data = inspect.getsourcelines(obj)

+         lineno = data[1]

+         order.append((lineno, data[0], name, obj))

+ 

+ 

+ dests = set()

+ for (lineno, source, name, obj) in sorted(order):

+     dest = get_dest(name, obj)

+     dests.add(dest)

+ outfiles = {}

+ for dest in dests:

+     if dest:

+         fn = os.path.join(destdir, dest)

+         outfiles[dest] = file(fn, 'w')

+ 

+ orig = file(filename).readlines()

+ ofs = 0

+ last_dest = None

+ for (lineno, source, name, obj) in sorted(order):

+     lineno -= 1   # make 0-indexed

+     dest = get_dest(name, obj)

+     if dest is None:

+         # the _ functions go different places

+         # defer (treat as intermediate content)

+         continue

+     if lineno > ofs:

+         # intermediate content

+         if last_dest == dest:

+             fo = outfiles[dest]

+         else:

+             fo = outfiles['cli/koji']

+         for line in orig[ofs:lineno]:

+             fo.write(line)

+     fo = outfiles[dest]

+     for line in source:

+         fo.write(line)

+     ofs = lineno + len(source)

+     last_dest = dest

+ 

+ sys.stderr.write('Orig: %i lines, ofs: %i\n' % (len(orig), ofs))

+ if len(orig) > ofs:

+     sys.stderr.write('Writing tail\n')

+     fo = outfiles['cli/koji']

+     for line in orig[ofs:]:

+         fo.write(line)

+ 

+ for dest in outfiles:

+     outfiles[dest].close()

+ 

+ open('cli/koji_cli/__init__.py', 'a+').close()

@@ -51,7 +51,6 @@ 

          restart-hosts             Restart enabled hosts

          revoke-cg-access          Remove a user from a content generator

          revoke-permission         Revoke a permission from a user

-         runroot                   Run a command in a buildroot

          set-build-volume          Move a build to a different volume

          set-pkg-arches            Set the list of extra arches for a package

          set-pkg-owner             Set the owner for a package

@@ -51,7 +51,6 @@ 

          restart-hosts             Restart enabled hosts

          revoke-cg-access          Remove a user from a content generator

          revoke-permission         Revoke a permission from a user

-         runroot                   Run a command in a buildroot

          set-build-volume          Move a build to a different volume

          set-pkg-arches            Set the list of extra arches for a package

          set-pkg-owner             Set the owner for a package
@@ -121,7 +120,6 @@ 

          dist-repo                 Create a yum repo with distribution options

          import-comps              Import group/package information from a comps file

          moshimoshi                Introduce yourself

-         save-failed-tree          Create tarball with whole buildtree

  

  monitor commands:

          wait-repo                 Wait for a repo to be regenerated

@@ -1,18 +1,14 @@ 

  from __future__ import absolute_import

- import unittest

- 

  

- import os

- 

- import sys

  

  import mock

+ import os

  import six

+ import sys

+ import unittest

  

- from . import loadcli

- 

- cli = loadcli.cli

  

+ from koji_cli.commands import handle_add_group

  

  class TestAddGroup(unittest.TestCase):

  
@@ -20,7 +16,7 @@ 

      maxDiff = None

  

      @mock.patch('sys.stdout', new_callable=six.StringIO)

-     @mock.patch('koji_cli.activate_session')

+     @mock.patch('koji_cli.commands.activate_session')

      def test_handle_add_group(self, activate_session_mock, stdout):

          tag = 'tag'

          group = 'group'
@@ -35,13 +31,13 @@ 

              {'name': 'otherGroup', 'group_id': 'otherGroupId'}]

  

          # Run it and check immediate output

-         rv = cli.handle_add_group(options, session, arguments)

+         rv = handle_add_group(options, session, arguments)

          actual = stdout.getvalue()

          expected = ''

          self.assertMultiLineEqual(actual, expected)

  

          # Finally, assert that things were called as we expected.

-         activate_session_mock.assert_called_once_with(session)

+         activate_session_mock.assert_called_once_with(session, options)

          session.hasPerm.assert_called_once_with('admin')

          session.getTag.assert_called_once_with(tag)

          session.getTagGroups.assert_called_once_with(tag, inherit=False)
@@ -49,7 +45,7 @@ 

          self.assertNotEqual(rv, 1)

  

      @mock.patch('sys.stdout', new_callable=six.StringIO)

-     @mock.patch('koji_cli.activate_session')

+     @mock.patch('koji_cli.commands.activate_session')

      def test_handle_add_group_dupl(self, activate_session_mock, stdout):

          tag = 'tag'

          group = 'group'
@@ -64,13 +60,13 @@ 

              {'name': 'group', 'group_id': 'groupId'}]

  

          # Run it and check immediate output

-         rv = cli.handle_add_group(options, session, arguments)

+         rv = handle_add_group(options, session, arguments)

          actual = stdout.getvalue()

          expected = 'Group group already exists for tag tag\n'

          self.assertMultiLineEqual(actual, expected)

  

          # Finally, assert that things were called as we expected.

-         activate_session_mock.assert_called_once_with(session)

+         activate_session_mock.assert_called_once_with(session, options)

          session.hasPerm.assert_called_once_with('admin')

          session.getTag.assert_called_once_with(tag)

          session.getTagGroups.assert_called_once_with(tag, inherit=False)
@@ -79,7 +75,7 @@ 

  

      @mock.patch('sys.stdout', new_callable=six.StringIO)

      @mock.patch('sys.stderr', new_callable=six.StringIO)

-     @mock.patch('koji_cli.activate_session')

+     @mock.patch('koji_cli.commands.activate_session')

      def test_handle_add_group_help(

              self,

              activate_session_mock,
@@ -93,7 +89,7 @@ 

  

          # Run it and check immediate output

          with self.assertRaises(SystemExit) as cm:

-             rv = cli.handle_add_group(options, session, arguments)

+             rv = handle_add_group(options, session, arguments)

          actual_stdout = stdout.getvalue()

          actual_stderr = stderr.getvalue()

          expected_stdout = ''
@@ -115,7 +111,7 @@ 

          self.assertEqual(cm.exception.code, 2)

  

      @mock.patch('sys.stdout', new_callable=six.StringIO)

-     @mock.patch('koji_cli.activate_session')

+     @mock.patch('koji_cli.commands.activate_session')

      def test_handle_add_group_no_perm(self, activate_session_mock, stdout):

          tag = 'tag'

          group = 'group'
@@ -127,13 +123,13 @@ 

          session.hasPerm.return_value = False

  

          # Run it and check immediate output

-         rv = cli.handle_add_group(options, session, arguments)

+         rv = handle_add_group(options, session, arguments)

          actual = stdout.getvalue()

          expected = 'This action requires admin privileges\n'

          self.assertMultiLineEqual(actual, expected)

  

          # Finally, assert that things were called as we expected.

-         activate_session_mock.assert_called_once_with(session)

+         activate_session_mock.assert_called_once_with(session, options)

          session.hasPerm.assert_called_once_with('admin')

          session.getTag.assert_not_called()

          session.getTagGroups.assert_not_called()
@@ -141,7 +137,7 @@ 

          self.assertEqual(rv, 1)

  

      @mock.patch('sys.stdout', new_callable=six.StringIO)

-     @mock.patch('koji_cli.activate_session')

+     @mock.patch('koji_cli.commands.activate_session')

      def test_handle_add_group_no_tag(self, activate_session_mock, stdout):

          tag = 'tag'

          group = 'group'
@@ -154,13 +150,13 @@ 

          session.getTag.return_value = None

  

          # Run it and check immediate output

-         rv = cli.handle_add_group(options, session, arguments)

+         rv = handle_add_group(options, session, arguments)

          actual = stdout.getvalue()

          expected = 'Unknown tag: tag\n'

          self.assertMultiLineEqual(actual, expected)

  

          # Finally, assert that things were called as we expected.

-         activate_session_mock.assert_called_once_with(session)

+         activate_session_mock.assert_called_once_with(session, options)

          session.hasPerm.assert_called_once_with('admin')

          session.getTag.assert_called_once_with(tag)

          session.getTagGroups.assert_not_called()

file modified
+18 -22
@@ -1,15 +1,11 @@ 

  from __future__ import absolute_import

- import unittest

- 

- import os

- import sys

  import mock

+ import os

  import six

+ import sys

+ import unittest

  

- from . import loadcli

- 

- cli = loadcli.cli

- 

+ from koji_cli.commands import handle_add_host

  

  class TestAddHost(unittest.TestCase):

  
@@ -17,7 +13,7 @@ 

      maxDiff = None

  

      @mock.patch('sys.stdout', new_callable=six.StringIO)

-     @mock.patch('koji_cli.activate_session')

+     @mock.patch('koji_cli.commands.activate_session')

      def test_handle_add_host(self, activate_session_mock, stdout):

          host = 'host'

          host_id = 1
@@ -36,18 +32,18 @@ 

          # Run it and check immediate output

          # args: host, arch1, arch2, --krb-principal=krb

          # expected: success

-         rv = cli.handle_add_host(options, session, arguments)

+         rv = handle_add_host(options, session, arguments)

          actual = stdout.getvalue()

          expected = 'host added: id 1\n'

          self.assertMultiLineEqual(actual, expected)

          # Finally, assert that things were called as we expected.

-         activate_session_mock.assert_called_once_with(session)

+         activate_session_mock.assert_called_once_with(session, options)

          session.getHost.assert_called_once_with(host)

          session.addHost.assert_called_once_with(host, arches, **kwargs)

          self.assertNotEqual(rv, 1)

  

      @mock.patch('sys.stdout', new_callable=six.StringIO)

-     @mock.patch('koji_cli.activate_session')

+     @mock.patch('koji_cli.commands.activate_session')

      def test_handle_add_host_no_krb_principal(

              self, activate_session_mock, stdout):

          host = 'host'
@@ -63,18 +59,18 @@ 

          # Run it and check immediate output

          # args: host, arch1, arch2

          # expected: success

-         rv = cli.handle_add_host(options, session, arguments)

+         rv = handle_add_host(options, session, arguments)

          actual = stdout.getvalue()

          expected = 'host added: id 1\n'

          self.assertMultiLineEqual(actual, expected)

          # Finally, assert that things were called as we expected.

-         activate_session_mock.assert_called_once_with(session)

+         activate_session_mock.assert_called_once_with(session, options)

          session.getHost.assert_called_once_with(host)

          session.addHost.assert_called_once_with(host, arches)

          self.assertNotEqual(rv, 1)

  

      @mock.patch('sys.stdout', new_callable=six.StringIO)

-     @mock.patch('koji_cli.activate_session')

+     @mock.patch('koji_cli.commands.activate_session')

      def test_handle_add_host_dupl(self, activate_session_mock, stdout):

          host = 'host'

          host_id = 1
@@ -90,19 +86,19 @@ 

          # Run it and check immediate output

          # args: host, arch1, arch2, --krb-principal=krb

          # expected: failed, host already exists

-         rv = cli.handle_add_host(options, session, arguments)

+         rv = handle_add_host(options, session, arguments)

          actual = stdout.getvalue()

          expected = 'host is already in the database\n'

          self.assertMultiLineEqual(actual, expected)

          # Finally, assert that things were called as we expected.

-         activate_session_mock.assert_called_once_with(session)

+         activate_session_mock.assert_called_once_with(session, options)

          session.getHost.assert_called_once_with(host)

          session.addHost.assert_not_called()

          self.assertEqual(rv, 1)

  

      @mock.patch('sys.stdout', new_callable=six.StringIO)

      @mock.patch('sys.stderr', new_callable=six.StringIO)

-     @mock.patch('koji_cli.activate_session')

+     @mock.patch('koji_cli.commands.activate_session')

      def test_handle_add_host_help(self, activate_session_mock, stderr, stdout):

          arguments = []

          options = mock.MagicMock()
@@ -113,7 +109,7 @@ 

  

          # Run it and check immediate output

          with self.assertRaises(SystemExit) as cm:

-             cli.handle_add_host(options, session, arguments)

+             handle_add_host(options, session, arguments)

          actual_stdout = stdout.getvalue()

          actual_stderr = stderr.getvalue()

          expected_stdout = ''
@@ -132,7 +128,7 @@ 

          self.assertEqual(cm.exception.code, 2)

  

      @mock.patch('sys.stdout', new_callable=six.StringIO)

-     @mock.patch('koji_cli.activate_session')

+     @mock.patch('koji_cli.commands.activate_session')

      def test_handle_add_host_failed(self, activate_session_mock, stdout):

          host = 'host'

          arches = ['arch1', 'arch2']
@@ -150,12 +146,12 @@ 

          # Run it and check immediate output

          # args: host, arch1, arch2, --krb-principal=krb

          # expected: failed

-         cli.handle_add_host(options, session, arguments)

+         handle_add_host(options, session, arguments)

          actual = stdout.getvalue()

          expected = ''

          self.assertMultiLineEqual(actual, expected)

          # Finally, assert that things were called as we expected.

-         activate_session_mock.assert_called_once_with(session)

+         activate_session_mock.assert_called_once_with(session, options)

          session.getHost.assert_called_once_with(host)

          session.addHost.assert_called_once_with(host, arches, **kwargs)

  

@@ -1,15 +1,12 @@ 

  from __future__ import absolute_import

- import unittest

  

- import os

- import sys

  import mock

+ import os

  import six

+ import sys

+ import unittest

  

- from . import loadcli

- 

- cli = loadcli.cli

- 

+ from koji_cli.commands import handle_add_host_to_channel

  

  class TestAddHostToChannel(unittest.TestCase):

  
@@ -17,7 +14,7 @@ 

      maxDiff = None

  

      @mock.patch('sys.stdout', new_callable=six.StringIO)

-     @mock.patch('koji_cli.activate_session')

+     @mock.patch('koji_cli.commands.activate_session')

      def test_handle_add_host_to_channel(self, activate_session_mock, stdout):

          host = 'host'

          host_info = mock.ANY
@@ -34,19 +31,19 @@ 

          # Run it and check immediate output

          # args: host, channel

          # expected: success

-         rv = cli.handle_add_host_to_channel(options, session, args)

+         rv = handle_add_host_to_channel(options, session, args)

          actual = stdout.getvalue()

          expected = ''

          self.assertMultiLineEqual(actual, expected)

          # Finally, assert that things were called as we expected.

-         activate_session_mock.assert_called_once_with(session)

+         activate_session_mock.assert_called_once_with(session, options)

          session.getChannel.assert_called_once_with(channel)

          session.getHost.assert_called_once_with(host)

          session.addHostToChannel.assert_called_once_with(host, channel)

          self.assertNotEqual(rv, 1)

  

      @mock.patch('sys.stdout', new_callable=six.StringIO)

-     @mock.patch('koji_cli.activate_session')

+     @mock.patch('koji_cli.commands.activate_session')

      def test_handle_add_host_to_channel_list(

              self, activate_session_mock, stdout):

          list_arg = '--list'
@@ -61,12 +58,12 @@ 

          # Run it and check immediate output

          # args: --list

          # expected: list all channel names

-         rv = cli.handle_add_host_to_channel(options, session, args)

+         rv = handle_add_host_to_channel(options, session, args)

          actual = stdout.getvalue()

          expected = 'channel1\nchannel2\n'

          self.assertMultiLineEqual(actual, expected)

          # Finally, assert that things were called as we expected.

-         activate_session_mock.assert_called_once_with(session)

+         activate_session_mock.assert_called_once_with(session, options)

          session.listChannels.assert_called_once()

          session.getChannel.assert_not_called()

          session.getHost.assert_not_called()
@@ -74,7 +71,7 @@ 

          self.assertNotEqual(rv, 1)

  

      @mock.patch('sys.stdout', new_callable=six.StringIO)

-     @mock.patch('koji_cli.activate_session')

+     @mock.patch('koji_cli.commands.activate_session')

      def test_handle_add_host_to_channel_new(

              self, activate_session_mock, stdout):

          host = 'host'
@@ -92,12 +89,12 @@ 

          # Run it and check immediate output

          # args: host, channel, --new

          # expected: success

-         rv = cli.handle_add_host_to_channel(options, session, args)

+         rv = handle_add_host_to_channel(options, session, args)

          actual = stdout.getvalue()

          expected = ''

          self.assertMultiLineEqual(actual, expected)

          # Finally, assert that things were called as we expected.

-         activate_session_mock.assert_called_once_with(session)

+         activate_session_mock.assert_called_once_with(session, options)

          session.getChannel.assert_not_called()

          session.getHost.assert_called_once_with(host)

          session.addHostToChannel.assert_called_once_with(
@@ -105,7 +102,7 @@ 

          self.assertNotEqual(rv, 1)

  

      @mock.patch('sys.stdout', new_callable=six.StringIO)

-     @mock.patch('koji_cli.activate_session')

+     @mock.patch('koji_cli.commands.activate_session')

      def test_handle_add_host_to_channel_no_channel(

              self, activate_session_mock, stdout):

          host = 'host'
@@ -121,19 +118,19 @@ 

          # Run it and check immediate output

          # args: host, channel

          # expected: failed, channel not found

-         rv = cli.handle_add_host_to_channel(options, session, args)

+         rv = handle_add_host_to_channel(options, session, args)

          actual = stdout.getvalue()

          expected = 'No such channel: channel\n'

          self.assertMultiLineEqual(actual, expected)

          # Finally, assert that things were called as we expected.

-         activate_session_mock.assert_called_once_with(session)

+         activate_session_mock.assert_called_once_with(session, options)

          session.getChannel.assert_called_once_with(channel)

          session.getHost.assert_not_called()

          session.addHostToChannel.assert_not_called()

          self.assertEqual(rv, 1)

  

      @mock.patch('sys.stdout', new_callable=six.StringIO)

-     @mock.patch('koji_cli.activate_session')

+     @mock.patch('koji_cli.commands.activate_session')

      def test_handle_add_host_to_channel_no_host(

              self, activate_session_mock, stdout):

          host = 'host'
@@ -151,12 +148,12 @@ 

          # Run it and check immediate output

          # args: host, channel

          # expected: success

-         rv = cli.handle_add_host_to_channel(options, session, args)

+         rv = handle_add_host_to_channel(options, session, args)

          actual = stdout.getvalue()

          expected = 'No such host: host\n'

          self.assertMultiLineEqual(actual, expected)

          # Finally, assert that things were called as we expected.

-         activate_session_mock.assert_called_once_with(session)

+         activate_session_mock.assert_called_once_with(session, options)

          session.getChannel.assert_called_once_with(channel)

          session.getHost.assert_called_once_with(host)

          session.addHostToChannel.assert_not_called()
@@ -164,7 +161,7 @@ 

  

      @mock.patch('sys.stdout', new_callable=six.StringIO)

      @mock.patch('sys.stderr', new_callable=six.StringIO)

-     @mock.patch('koji_cli.activate_session')

+     @mock.patch('koji_cli.commands.activate_session')

      def test_handle_add_host_to_channel_help(

              self, activate_session_mock, stderr, stdout):

          args = []
@@ -178,7 +175,7 @@ 

          # args: _empty_

          # expected: failed, help msg shows

          with self.assertRaises(SystemExit) as cm:

-             cli.handle_add_host_to_channel(options, session, args)

+             handle_add_host_to_channel(options, session, args)

          actual_stdout = stdout.getvalue()

          actual_stderr = stderr.getvalue()

          expected_stdout = ''

file modified
+19 -21
@@ -1,16 +1,14 @@ 

  from __future__ import absolute_import

- import unittest

  

- import os

- import sys

  import mock

+ import os

  import six

+ import sys

+ import unittest

  

  from mock import call

  

- from . import loadcli

- 

- cli = loadcli.cli

+ from koji_cli.commands import handle_add_pkg

  

  

  class TestAddPkg(unittest.TestCase):
@@ -19,7 +17,7 @@ 

      maxDiff = None

  

      @mock.patch('sys.stdout', new_callable=six.StringIO)

-     @mock.patch('koji_cli.activate_session')

+     @mock.patch('koji_cli.commands.activate_session')

      def test_handle_add_pkg(self, activate_session_mock, stdout):

          tag = 'tag'

          dsttag = {'name': tag, 'id': 1}
@@ -49,12 +47,12 @@ 

          # Run it and check immediate output

          # args: --owner, --extra-arches='arch1,arch2 arch3, arch4', tag, package

          # expected: success

-         rv = cli.handle_add_pkg(options, session, args)

+         rv = handle_add_pkg(options, session, args)

          actual = stdout.getvalue()

          expected = 'Adding 1 packages to tag tag\n'

          self.assertMultiLineEqual(actual, expected)

          # Finally, assert that things were called as we expected.

-         activate_session_mock.assert_called_once_with(session)

+         activate_session_mock.assert_called_once_with(session, options)

          session.getUser.assert_called_once_with(owner)

          session.getTag.assert_called_once_with(tag)

          session.listPackages.assert_called_once_with(tagID=dsttag['id'])
@@ -64,7 +62,7 @@ 

          self.assertNotEqual(rv, 1)

  

      @mock.patch('sys.stdout', new_callable=six.StringIO)

-     @mock.patch('koji_cli.activate_session')

+     @mock.patch('koji_cli.commands.activate_session')

      def test_handle_add_pkg_multi_pkg(self, activate_session_mock, stdout):

          tag = 'tag'

          dsttag = {'name': tag, 'id': 1}
@@ -92,12 +90,12 @@ 

          # args: --owner, --extra-arches='arch1,arch2 arch3, arch4',

          #       tag, package1, package2, package3

          # expected: success

-         rv = cli.handle_add_pkg(options, session, args)

+         rv = handle_add_pkg(options, session, args)

          actual = stdout.getvalue()

          expected = 'Package package2 already exists in tag tag\nAdding 2 packages to tag tag\n'

          self.assertMultiLineEqual(actual, expected)

          # Finally, assert that things were called as we expected.

-         activate_session_mock.assert_called_once_with(session)

+         activate_session_mock.assert_called_once_with(session, options)

          self.assertEqual(session.mock_calls,

                           [call.getUser(owner),

                            call.getTag(tag),
@@ -108,7 +106,7 @@ 

          self.assertNotEqual(rv, 1)

  

      @mock.patch('sys.stdout', new_callable=six.StringIO)

-     @mock.patch('koji_cli.activate_session')

+     @mock.patch('koji_cli.commands.activate_session')

      def test_handle_add_pkg_owner_no_exists(

              self, activate_session_mock, stdout):

          tag = 'tag'
@@ -130,7 +128,7 @@ 

          # args: --owner, --extra-arches='arch1,arch2 arch3, arch4',

          #       tag, package1, package2, package3

          # expected: failed: owner does not exist

-         rv = cli.handle_add_pkg(options, session, args)

+         rv = handle_add_pkg(options, session, args)

          actual = stdout.getvalue()

          expected = 'User owner does not exist\n'

          self.assertMultiLineEqual(actual, expected)
@@ -141,7 +139,7 @@ 

          self.assertEqual(rv, 1)

  

      @mock.patch('sys.stdout', new_callable=six.StringIO)

-     @mock.patch('koji_cli.activate_session')

+     @mock.patch('koji_cli.commands.activate_session')

      def test_handle_add_pkg_tag_no_exists(self, activate_session_mock, stdout):

          tag = 'tag'

          dsttag = None
@@ -165,12 +163,12 @@ 

          #       tag, package1, package2, package3

          # expected: failed: tag does not exist

          with self.assertRaises(SystemExit) as cm:

-             cli.handle_add_pkg(options, session, args)

+             handle_add_pkg(options, session, args)

          actual = stdout.getvalue()

          expected = 'No such tag: tag\n'

          self.assertMultiLineEqual(actual, expected)

          # Finally, assert that things were called as we expected.

-         activate_session_mock.assert_called_once_with(session)

+         activate_session_mock.assert_called_once_with(session, options)

          self.assertEqual(session.mock_calls,

                           [call.getUser(owner),

                            call.getTag(tag)])
@@ -178,7 +176,7 @@ 

  

      @mock.patch('sys.stdout', new_callable=six.StringIO)

      @mock.patch('sys.stderr', new_callable=six.StringIO)

-     @mock.patch('koji_cli.activate_session')

+     @mock.patch('koji_cli.commands.activate_session')

      def test_handle_add_pkg_no_owner(

              self, activate_session_mock, stderr, stdout):

          tag = 'tag'
@@ -194,7 +192,7 @@ 

  

          # Run it and check immediate output

          with self.assertRaises(SystemExit) as cm:

-             cli.handle_add_pkg(options, session, args)

+             handle_add_pkg(options, session, args)

          actual_stdout = stdout.getvalue()

          actual_stderr = stderr.getvalue()

          expected_stdout = ''
@@ -216,7 +214,7 @@ 

  

      @mock.patch('sys.stdout', new_callable=six.StringIO)

      @mock.patch('sys.stderr', new_callable=six.StringIO)

-     @mock.patch('koji_cli.activate_session')

+     @mock.patch('koji_cli.commands.activate_session')

      def test_handle_add_pkg_no_arg(

              self, activate_session_mock, stderr, stdout):

          args = []
@@ -228,7 +226,7 @@ 

  

          # Run it and check immediate output

          with self.assertRaises(SystemExit) as cm:

-             cli.handle_add_pkg(options, session, args)

+             handle_add_pkg(options, session, args)

          actual_stdout = stdout.getvalue()

          actual_stderr = stderr.getvalue()

          expected_stdout = ''

@@ -1,16 +1,13 @@ 

  from __future__ import absolute_import

- import unittest

- 

- import os

- import sys

  import mock

+ import os

  import six

+ import sys

+ import unittest

  

  from mock import call

  

- from . import loadcli

- 

- cli = loadcli.cli

+ from koji_cli.commands import handle_block_pkg

  

  

  class TestBlockPkg(unittest.TestCase):
@@ -19,7 +16,7 @@ 

      maxDiff = None

  

      @mock.patch('sys.stdout', new_callable=six.StringIO)

-     @mock.patch('koji_cli.activate_session')

+     @mock.patch('koji_cli.commands.activate_session')

      def test_handle_block_pkg(self, activate_session_mock, stdout):

          tag = 'tag'

          dsttag = {'name': tag, 'id': 1}
@@ -36,12 +33,12 @@ 

          # Run it and check immediate output

          # args: tag, package

          # expected: success

-         rv = cli.handle_block_pkg(options, session, args)

+         rv = handle_block_pkg(options, session, args)

          actual = stdout.getvalue()

          expected = ''

          self.assertMultiLineEqual(actual, expected)

          # Finally, assert that things were called as we expected.

-         activate_session_mock.assert_called_once_with(session)

+         activate_session_mock.assert_called_once_with(session, options)

          session.getTag.assert_called_once_with(tag)

          session.listPackages.assert_called_once_with(

              tagID=dsttag['id'], inherited=True)
@@ -51,7 +48,7 @@ 

          self.assertNotEqual(rv, 1)

  

      @mock.patch('sys.stdout', new_callable=six.StringIO)

-     @mock.patch('koji_cli.activate_session')

+     @mock.patch('koji_cli.commands.activate_session')

      def test_handle_block_pkg_multi_pkg(self, activate_session_mock, stdout):

          tag = 'tag'

          dsttag = {'name': tag, 'id': 1}
@@ -72,12 +69,12 @@ 

          # Run it and check immediate output

          # args: tag, package1, package2, package3

          # expected: success

-         rv = cli.handle_block_pkg(options, session, args)

+         rv = handle_block_pkg(options, session, args)

          actual = stdout.getvalue()

          expected = ''

          self.assertMultiLineEqual(actual, expected)

          # Finally, assert that things were called as we expected.

-         activate_session_mock.assert_called_once_with(session)

+         activate_session_mock.assert_called_once_with(session, options)

          self.assertEqual(

              session.mock_calls, [

                  call.getTag(tag), call.listPackages(
@@ -89,7 +86,7 @@ 

          self.assertNotEqual(rv, 1)

  

      @mock.patch('sys.stdout', new_callable=six.StringIO)

-     @mock.patch('koji_cli.activate_session')

+     @mock.patch('koji_cli.commands.activate_session')

      def test_handle_block_pkg_no_package(self, activate_session_mock, stdout):

          tag = 'tag'

          dsttag = {'name': tag, 'id': 1}
@@ -108,12 +105,12 @@ 

          # Run it and check immediate output

          # args: tag, package1, package2, package3

          # expected: failed: can not find package2 under tag

-         rv = cli.handle_block_pkg(options, session, args)

+         rv = handle_block_pkg(options, session, args)

          actual = stdout.getvalue()

          expected = 'Package package2 doesn\'t exist in tag tag\n'

          self.assertMultiLineEqual(actual, expected)

          # Finally, assert that things were called as we expected.

-         activate_session_mock.assert_called_once_with(session)

+         activate_session_mock.assert_called_once_with(session, options)

          session.getTag.assert_called_once_with(tag)

          session.listPackages.assert_called_once_with(

              tagID=dsttag['id'], inherited=True)
@@ -122,7 +119,7 @@ 

          self.assertEqual(rv, 1)

  

      @mock.patch('sys.stdout', new_callable=six.StringIO)

-     @mock.patch('koji_cli.activate_session')

+     @mock.patch('koji_cli.commands.activate_session')

      def test_handle_block_pkg_tag_no_exists(

              self, activate_session_mock, stdout):

          tag = 'tag'
@@ -138,12 +135,12 @@ 

          # Run it and check immediate output

          # args: tag, package1, package2, package3

          # expected: failed: tag does not exist

-         rv = cli.handle_block_pkg(options, session, args)

+         rv = handle_block_pkg(options, session, args)

          actual = stdout.getvalue()

          expected = 'No such tag: tag\n'

          self.assertMultiLineEqual(actual, expected)

          # Finally, assert that things were called as we expected.

-         activate_session_mock.assert_called_once_with(session)

+         activate_session_mock.assert_called_once_with(session, options)

          session.getTag.assert_called_once_with(tag)

          session.listPackages.assert_not_called()

          session.packageListBlock.assert_not_called()
@@ -151,7 +148,7 @@ 

  

      @mock.patch('sys.stdout', new_callable=six.StringIO)

      @mock.patch('sys.stderr', new_callable=six.StringIO)

-     @mock.patch('koji_cli.activate_session')

+     @mock.patch('koji_cli.commands.activate_session')

      def test_handle_block_pkg_help(

              self, activate_session_mock, stderr, stdout):

          args = []
@@ -164,7 +161,7 @@ 

  

          # Run it and check immediate output

          with self.assertRaises(SystemExit) as cm:

-             cli.handle_block_pkg(options, session, args)

+             handle_block_pkg(options, session, args)

          actual_stdout = stdout.getvalue()

          actual_stderr = stderr.getvalue()

          expected_stdout = ''

file modified
+117 -112
@@ -1,15 +1,11 @@ 

  from __future__ import absolute_import

- import unittest

- 

- import os

- import sys

  import mock

+ import os

  import six

+ import sys

+ import unittest

  

- from . import loadcli

- 

- cli = loadcli.cli

- 

+ from koji_cli.commands import handle_build, _progress_callback

  

  class TestBuild(unittest.TestCase):

      # Show long diffs in error output...
@@ -20,14 +16,15 @@ 

          self.options = mock.MagicMock()

          self.options.quiet = None

          self.options.weburl = 'weburl'

+         self.options.poll_interval = 0

          # Mock out the xmlrpc server

          self.session = mock.MagicMock()

  

      @mock.patch('sys.stdout', new_callable=six.StringIO)

-     @mock.patch('koji_cli.activate_session')

-     @mock.patch('koji_cli._unique_path', return_value='random_path')

-     @mock.patch('koji_cli._running_in_bg', return_value=False)

-     @mock.patch('koji_cli.watch_tasks', return_value=0)

+     @mock.patch('koji_cli.commands.activate_session')

+     @mock.patch('koji_cli.commands._unique_path', return_value='random_path')

+     @mock.patch('koji_cli.commands._running_in_bg', return_value=False)

+     @mock.patch('koji_cli.commands.watch_tasks', return_value=0)

      def test_handle_build_from_srpm(

              self,

              watch_tasks_mock,
@@ -51,7 +48,7 @@ 

          # Run it and check immediate output

          # args: target, srpm

          # expected: success

-         rv = cli.handle_build(self.options, self.session, args)

+         rv = handle_build(self.options, self.session, args)

          actual = stdout.getvalue()

          expected = """Uploading srpm: srpm

  
@@ -60,25 +57,26 @@ 

  """

          self.assertMultiLineEqual(actual, expected)

          # Finally, assert that things were called as we expected.

-         activate_session_mock.assert_called_once_with(self.session)

+         activate_session_mock.assert_called_once_with(self.session, self.options)

          self.session.getBuildTarget.assert_called_once_with(target)

          self.session.getTag.assert_called_once_with(dest_tag)

          unique_path_mock.assert_called_once_with('cli-build')

          self.assertEqual(running_in_bg_mock.call_count, 2)

          self.session.uploadWrapper.assert_called_once_with(

-             source, 'random_path', callback=cli._progress_callback)

+             source, 'random_path', callback=_progress_callback)

          self.session.build.assert_called_once_with(

              'random_path/' + source, target, opts, priority=priority)

          self.session.logout.assert_called()

          watch_tasks_mock.assert_called_once_with(

-             self.session, [task_id], quiet=self.options.quiet)

+             self.session, [task_id], quiet=self.options.quiet,

+             poll_interval=self.options.poll_interval)

          self.assertEqual(rv, 0)

  

      @mock.patch('sys.stdout', new_callable=six.StringIO)

-     @mock.patch('koji_cli.activate_session')

-     @mock.patch('koji_cli._unique_path', return_value='random_path')

-     @mock.patch('koji_cli._running_in_bg', return_value=False)

-     @mock.patch('koji_cli.watch_tasks', return_value=0)

+     @mock.patch('koji_cli.commands.activate_session')

+     @mock.patch('koji_cli.commands._unique_path', return_value='random_path')

+     @mock.patch('koji_cli.commands._running_in_bg', return_value=False)

+     @mock.patch('koji_cli.commands.watch_tasks', return_value=0)

      def test_handle_build_from_scm(

              self,

              watch_tasks_mock,
@@ -102,14 +100,14 @@ 

          # Run it and check immediate output

          # args: target, http://scm

          # expected: success

-         rv = cli.handle_build(self.options, self.session, args)

+         rv = handle_build(self.options, self.session, args)

          actual = stdout.getvalue()

          expected = """Created task: 1

  Task info: weburl/taskinfo?taskID=1

  """

          self.assertMultiLineEqual(actual, expected)

          # Finally, assert that things were called as we expected.

-         activate_session_mock.assert_called_once_with(self.session)

+         activate_session_mock.assert_called_once_with(self.session, self.options)

          self.session.getBuildTarget.assert_called_once_with(target)

          self.session.getTag.assert_called_once_with(dest_tag)

          unique_path_mock.assert_not_called()
@@ -119,15 +117,16 @@ 

              source, target, opts, priority=priority)

          self.session.logout.assert_called()

          watch_tasks_mock.assert_called_once_with(

-             self.session, [task_id], quiet=self.options.quiet)

+             self.session, [task_id], quiet=self.options.quiet,

+             poll_interval=self.options.poll_interval)

          self.assertEqual(rv, 0)

  

      @mock.patch('sys.stdout', new_callable=six.StringIO)

      @mock.patch('sys.stderr', new_callable=six.StringIO)

-     @mock.patch('koji_cli.activate_session')

-     @mock.patch('koji_cli._unique_path', return_value='random_path')

-     @mock.patch('koji_cli._running_in_bg', return_value=False)

-     @mock.patch('koji_cli.watch_tasks', return_value=0)

+     @mock.patch('koji_cli.commands.activate_session')

+     @mock.patch('koji_cli.commands._unique_path', return_value='random_path')

+     @mock.patch('koji_cli.commands._running_in_bg', return_value=False)

+     @mock.patch('koji_cli.commands.watch_tasks', return_value=0)

      def test_handle_build_no_arg(

              self,

              watch_tasks_mock,
@@ -141,7 +140,7 @@ 

  

          # Run it and check immediate output

          with self.assertRaises(SystemExit) as cm:

-             cli.handle_build(self.options, self.session, args)

+             handle_build(self.options, self.session, args)

          actual_stdout = stdout.getvalue()

          actual_stderr = stderr.getvalue()

          expected_stdout = ''
@@ -167,10 +166,10 @@ 

  

      @mock.patch('sys.stdout', new_callable=six.StringIO)

      @mock.patch('sys.stderr', new_callable=six.StringIO)

-     @mock.patch('koji_cli.activate_session')

-     @mock.patch('koji_cli._unique_path', return_value='random_path')

-     @mock.patch('koji_cli._running_in_bg', return_value=False)

-     @mock.patch('koji_cli.watch_tasks', return_value=0)

+     @mock.patch('koji_cli.commands.activate_session')

+     @mock.patch('koji_cli.commands._unique_path', return_value='random_path')

+     @mock.patch('koji_cli.commands._running_in_bg', return_value=False)

+     @mock.patch('koji_cli.commands.watch_tasks', return_value=0)

      def test_handle_build_help(

              self,

              watch_tasks_mock,
@@ -184,7 +183,7 @@ 

  

          # Run it and check immediate output

          with self.assertRaises(SystemExit) as cm:

-             cli.handle_build(self.options, self.session, args)

+             handle_build(self.options, self.session, args)

          actual_stdout = stdout.getvalue()

          actual_stderr = stderr.getvalue()

          expected_stdout = """Usage: %s build [options] target <srpm path or scm url>
@@ -221,10 +220,10 @@ 

  

      @mock.patch('sys.stdout', new_callable=six.StringIO)

      @mock.patch('sys.stderr', new_callable=six.StringIO)

-     @mock.patch('koji_cli.activate_session')

-     @mock.patch('koji_cli._unique_path', return_value='random_path')

-     @mock.patch('koji_cli._running_in_bg', return_value=False)

-     @mock.patch('koji_cli.watch_tasks', return_value=0)

+     @mock.patch('koji_cli.commands.activate_session')

+     @mock.patch('koji_cli.commands._unique_path', return_value='random_path')

+     @mock.patch('koji_cli.commands._running_in_bg', return_value=False)

+     @mock.patch('koji_cli.commands.watch_tasks', return_value=0)

      def test_handle_build_arch_override_denied(

              self,

              watch_tasks_mock,
@@ -241,7 +240,7 @@ 

  

          # Run it and check immediate output

          with self.assertRaises(SystemExit) as cm:

-             cli.handle_build(self.options, self.session, args)

+             handle_build(self.options, self.session, args)

          actual_stdout = stdout.getvalue()

          actual_stderr = stderr.getvalue()

          expected_stdout = ''
@@ -266,10 +265,10 @@ 

          self.assertEqual(cm.exception.code, 2)

  

      @mock.patch('sys.stdout', new_callable=six.StringIO)

-     @mock.patch('koji_cli.activate_session')

-     @mock.patch('koji_cli._unique_path', return_value='random_path')

-     @mock.patch('koji_cli._running_in_bg', return_value=False)

-     @mock.patch('koji_cli.watch_tasks', return_value=0)

+     @mock.patch('koji_cli.commands.activate_session')

+     @mock.patch('koji_cli.commands._unique_path', return_value='random_path')

+     @mock.patch('koji_cli.commands._running_in_bg', return_value=False)

+     @mock.patch('koji_cli.commands.watch_tasks', return_value=0)

      def test_handle_build_none_tag(

              self,

              watch_tasks_mock,
@@ -289,14 +288,14 @@ 

          # Run it and check immediate output

          # args: --repo-id=2, nOne, http://scm

          # expected: success

-         rv = cli.handle_build(self.options, self.session, args)

+         rv = handle_build(self.options, self.session, args)

          actual = stdout.getvalue()

          expected = """Created task: 1

  Task info: weburl/taskinfo?taskID=1

  """

          self.assertMultiLineEqual(actual, expected)

          # Finally, assert that things were called as we expected.

-         activate_session_mock.assert_called_once_with(self.session)

+         activate_session_mock.assert_called_once_with(self.session, self.options)

          self.session.getBuildTarget.assert_not_called()

          self.session.getTag.assert_not_called()

          unique_path_mock.assert_not_called()
@@ -307,14 +306,15 @@ 

              source, None, opts, priority=priority)

          self.session.logout.assert_called()

          watch_tasks_mock.assert_called_once_with(

-             self.session, [task_id], quiet=self.options.quiet)

+             self.session, [task_id], quiet=self.options.quiet,

+             poll_interval=self.options.poll_interval)

          self.assertEqual(rv, 0)

  

      @mock.patch('sys.stderr', new_callable=six.StringIO)

-     @mock.patch('koji_cli.activate_session')

-     @mock.patch('koji_cli._unique_path', return_value='random_path')

-     @mock.patch('koji_cli._running_in_bg', return_value=False)

-     @mock.patch('koji_cli.watch_tasks', return_value=0)

+     @mock.patch('koji_cli.commands.activate_session')

+     @mock.patch('koji_cli.commands._unique_path', return_value='random_path')

+     @mock.patch('koji_cli.commands._running_in_bg', return_value=False)

+     @mock.patch('koji_cli.commands.watch_tasks', return_value=0)

      def test_handle_build_target_not_found(

              self,

              watch_tasks_mock,
@@ -334,7 +334,7 @@ 

          # args: target, http://scm

          # expected: failed, target not found

          with self.assertRaises(SystemExit) as cm:

-             cli.handle_build(self.options, self.session, args)

+             handle_build(self.options, self.session, args)

          actual = stderr.getvalue()

          expected = """Usage: %s build [options] target <srpm path or scm url>

  (Specify the --help global option for a list of other help options)
@@ -343,7 +343,7 @@ 

  """ % (progname, progname)

          self.assertMultiLineEqual(actual, expected)

          # Finally, assert that things were called as we expected.

-         activate_session_mock.assert_called_once_with(self.session)

+         activate_session_mock.assert_called_once_with(self.session, self.options)

          self.session.getBuildTarget.assert_called_once_with(target)

          self.session.getTag.assert_not_called()

          unique_path_mock.assert_not_called()
@@ -355,10 +355,10 @@ 

          self.assertEqual(cm.exception.code, 2)

  

      @mock.patch('sys.stderr', new_callable=six.StringIO)

-     @mock.patch('koji_cli.activate_session')

-     @mock.patch('koji_cli._unique_path', return_value='random_path')

-     @mock.patch('koji_cli._running_in_bg', return_value=False)

-     @mock.patch('koji_cli.watch_tasks', return_value=0)

+     @mock.patch('koji_cli.commands.activate_session')

+     @mock.patch('koji_cli.commands._unique_path', return_value='random_path')

+     @mock.patch('koji_cli.commands._running_in_bg', return_value=False)

+     @mock.patch('koji_cli.commands.watch_tasks', return_value=0)

      def test_handle_build_dest_tag_not_found(

              self,

              watch_tasks_mock,
@@ -382,7 +382,7 @@ 

          # args: target, http://scm

          # expected: failed, dest_tag not found

          with self.assertRaises(SystemExit) as cm:

-             cli.handle_build(self.options, self.session, args)

+             handle_build(self.options, self.session, args)

          actual = stderr.getvalue()

          expected = """Usage: %s build [options] target <srpm path or scm url>

  (Specify the --help global option for a list of other help options)
@@ -391,7 +391,7 @@ 

  """ % (progname, progname)

          self.assertMultiLineEqual(actual, expected)

          # Finally, assert that things were called as we expected.

-         activate_session_mock.assert_called_once_with(self.session)

+         activate_session_mock.assert_called_once_with(self.session, self.options)

          self.session.getBuildTarget.assert_called_once_with(target)

          self.session.getTag.assert_called_once_with(dest_tag)

          unique_path_mock.assert_not_called()
@@ -403,10 +403,10 @@ 

          self.assertEqual(cm.exception.code, 2)

  

      @mock.patch('sys.stderr', new_callable=six.StringIO)

-     @mock.patch('koji_cli.activate_session')

-     @mock.patch('koji_cli._unique_path', return_value='random_path')

-     @mock.patch('koji_cli._running_in_bg', return_value=False)

-     @mock.patch('koji_cli.watch_tasks', return_value=0)

+     @mock.patch('koji_cli.commands.activate_session')

+     @mock.patch('koji_cli.commands._unique_path', return_value='random_path')

+     @mock.patch('koji_cli.commands._running_in_bg', return_value=False)

+     @mock.patch('koji_cli.commands.watch_tasks', return_value=0)

      def test_handle_build_dest_tag_locked(

              self,

              watch_tasks_mock,
@@ -430,7 +430,7 @@ 

          # args: target, http://scm

          # expected: failed, dest_tag is locked

          with self.assertRaises(SystemExit) as cm:

-             cli.handle_build(self.options, self.session, args)

+             handle_build(self.options, self.session, args)

          actual = stderr.getvalue()

          expected = """Usage: %s build [options] target <srpm path or scm url>

  (Specify the --help global option for a list of other help options)
@@ -439,7 +439,7 @@ 

  """ % (progname, progname)

          self.assertMultiLineEqual(actual, expected)

          # Finally, assert that things were called as we expected.

-         activate_session_mock.assert_called_once_with(self.session)

+         activate_session_mock.assert_called_once_with(self.session, self.options)

          self.session.getBuildTarget.assert_called_once_with(target)

          self.session.getTag.assert_called_once_with(dest_tag)

          unique_path_mock.assert_not_called()
@@ -451,10 +451,10 @@ 

          self.assertEqual(cm.exception.code, 2)

  

      @mock.patch('sys.stdout', new_callable=six.StringIO)

-     @mock.patch('koji_cli.activate_session')

-     @mock.patch('koji_cli._unique_path', return_value='random_path')

-     @mock.patch('koji_cli._running_in_bg', return_value=False)

-     @mock.patch('koji_cli.watch_tasks', return_value=0)

+     @mock.patch('koji_cli.commands.activate_session')

+     @mock.patch('koji_cli.commands._unique_path', return_value='random_path')

+     @mock.patch('koji_cli.commands._running_in_bg', return_value=False)

+     @mock.patch('koji_cli.commands.watch_tasks', return_value=0)

      def test_handle_build_arch_override(

              self,

              watch_tasks_mock,
@@ -484,14 +484,14 @@ 

          # Run it and check immediate output

          # args: --arch-override=somearch, --scratch, target, http://scm

          # expected: success

-         rv = cli.handle_build(self.options, self.session, args)

+         rv = handle_build(self.options, self.session, args)

          actual = stdout.getvalue()

          expected = """Created task: 1

  Task info: weburl/taskinfo?taskID=1

  """

          self.assertMultiLineEqual(actual, expected)

          # Finally, assert that things were called as we expected.

-         activate_session_mock.assert_called_once_with(self.session)

+         activate_session_mock.assert_called_once_with(self.session, self.options)

          self.session.getBuildTarget.assert_called_once_with(target)

          self.session.getTag.assert_called_once_with(dest_tag)

          unique_path_mock.assert_not_called()
@@ -502,14 +502,15 @@ 

              source, target, opts, priority=priority)

          self.session.logout.assert_called()

          watch_tasks_mock.assert_called_once_with(

-             self.session, [task_id], quiet=self.options.quiet)

+             self.session, [task_id], quiet=self.options.quiet,

+             poll_interval=self.options.poll_interval)

          self.assertEqual(rv, 0)

  

      @mock.patch('sys.stdout', new_callable=six.StringIO)

-     @mock.patch('koji_cli.activate_session')

-     @mock.patch('koji_cli._unique_path', return_value='random_path')

-     @mock.patch('koji_cli._running_in_bg', return_value=False)

-     @mock.patch('koji_cli.watch_tasks', return_value=0)

+     @mock.patch('koji_cli.commands.activate_session')

+     @mock.patch('koji_cli.commands._unique_path', return_value='random_path')

+     @mock.patch('koji_cli.commands._running_in_bg', return_value=False)

+     @mock.patch('koji_cli.commands.watch_tasks', return_value=0)

      def test_handle_build_background(

              self,

              watch_tasks_mock,
@@ -533,14 +534,14 @@ 

          # Run it and check immediate output

          # args: --background, target, http://scm

          # expected: success

-         rv = cli.handle_build(self.options, self.session, args)

+         rv = handle_build(self.options, self.session, args)

          actual = stdout.getvalue()

          expected = """Created task: 1

  Task info: weburl/taskinfo?taskID=1

  """

          self.assertMultiLineEqual(actual, expected)

          # Finally, assert that things were called as we expected.

-         activate_session_mock.assert_called_once_with(self.session)

+         activate_session_mock.assert_called_once_with(self.session, self.options)

          self.session.getBuildTarget.assert_called_once_with(target)

          self.session.getTag.assert_called_once_with(dest_tag)

          unique_path_mock.assert_not_called()
@@ -550,14 +551,15 @@ 

              source, target, opts, priority=priority)

          self.session.logout.assert_called()

          watch_tasks_mock.assert_called_once_with(

-             self.session, [task_id], quiet=self.options.quiet)

+             self.session, [task_id], quiet=self.options.quiet,

+             poll_interval=self.options.poll_interval)

          self.assertEqual(rv, 0)

  

      @mock.patch('sys.stdout', new_callable=six.StringIO)

-     @mock.patch('koji_cli.activate_session')

-     @mock.patch('koji_cli._unique_path', return_value='random_path')

-     @mock.patch('koji_cli._running_in_bg', return_value=True)

-     @mock.patch('koji_cli.watch_tasks', return_value=0)

+     @mock.patch('koji_cli.commands.activate_session')

+     @mock.patch('koji_cli.commands._unique_path', return_value='random_path')

+     @mock.patch('koji_cli.commands._running_in_bg', return_value=True)

+     @mock.patch('koji_cli.commands.watch_tasks', return_value=0)

      def test_handle_build_running_in_bg(

              self,

              watch_tasks_mock,
@@ -581,7 +583,7 @@ 

          # Run it and check immediate output

          # args: target, srpm

          # expected: success

-         rv = cli.handle_build(self.options, self.session, args)

+         rv = handle_build(self.options, self.session, args)

          actual = stdout.getvalue()

          expected = """Uploading srpm: srpm

  
@@ -590,7 +592,7 @@ 

  """

          self.assertMultiLineEqual(actual, expected)

          # Finally, assert that things were called as we expected.

-         activate_session_mock.assert_called_once_with(self.session)

+         activate_session_mock.assert_called_once_with(self.session, self.options)

          self.session.getBuildTarget.assert_called_once_with(target)

          self.session.getTag.assert_called_once_with(dest_tag)

          unique_path_mock.assert_called_once_with('cli-build')
@@ -605,10 +607,10 @@ 

          self.assertIsNone(rv)

  

      @mock.patch('sys.stdout', new_callable=six.StringIO)

-     @mock.patch('koji_cli.activate_session')

-     @mock.patch('koji_cli._unique_path', return_value='random_path')

-     @mock.patch('koji_cli._running_in_bg', return_value=False)

-     @mock.patch('koji_cli.watch_tasks', return_value=0)

+     @mock.patch('koji_cli.commands.activate_session')

+     @mock.patch('koji_cli.commands._unique_path', return_value='random_path')

+     @mock.patch('koji_cli.commands._running_in_bg', return_value=False)

+     @mock.patch('koji_cli.commands.watch_tasks', return_value=0)

      def test_handle_build_noprogress(

              self,

              watch_tasks_mock,
@@ -632,7 +634,7 @@ 

          # Run it and check immediate output

          # args: --noprogress, target, srpm

          # expected: success

-         rv = cli.handle_build(self.options, self.session, args)

+         rv = handle_build(self.options, self.session, args)

          actual = stdout.getvalue()

          expected = """Uploading srpm: srpm

  
@@ -641,7 +643,7 @@ 

  """

          self.assertMultiLineEqual(actual, expected)

          # Finally, assert that things were called as we expected.

-         activate_session_mock.assert_called_once_with(self.session)

+         activate_session_mock.assert_called_once_with(self.session, self.options)

          self.session.getBuildTarget.assert_called_once_with(target)

          self.session.getTag.assert_called_once_with(dest_tag)

          unique_path_mock.assert_called_once_with('cli-build')
@@ -653,14 +655,15 @@ 

              'random_path/' + source, target, opts, priority=priority)

          self.session.logout.assert_called_once()

          watch_tasks_mock.assert_called_once_with(

-             self.session, [task_id], quiet=self.options.quiet)

+             self.session, [task_id], quiet=self.options.quiet,

+             poll_interval=self.options.poll_interval)

          self.assertEqual(rv, 0)

  

      @mock.patch('sys.stdout', new_callable=six.StringIO)

-     @mock.patch('koji_cli.activate_session')

-     @mock.patch('koji_cli._unique_path', return_value='random_path')

-     @mock.patch('koji_cli._running_in_bg', return_value=False)

-     @mock.patch('koji_cli.watch_tasks', return_value=0)

+     @mock.patch('koji_cli.commands.activate_session')

+     @mock.patch('koji_cli.commands._unique_path', return_value='random_path')

+     @mock.patch('koji_cli.commands._running_in_bg', return_value=False)

+     @mock.patch('koji_cli.commands.watch_tasks', return_value=0)

      def test_handle_build_quiet(

              self,

              watch_tasks_mock,
@@ -685,12 +688,12 @@ 

          # Run it and check immediate output

          # args: --quiet, target, srpm

          # expected: success

-         rv = cli.handle_build(self.options, self.session, args)

+         rv = handle_build(self.options, self.session, args)

          actual = stdout.getvalue()

          expected = '\n'

          self.assertMultiLineEqual(actual, expected)

          # Finally, assert that things were called as we expected.

-         activate_session_mock.assert_called_once_with(self.session)

+         activate_session_mock.assert_called_once_with(self.session, self.options)

          self.session.getBuildTarget.assert_called_once_with(target)

          self.session.getTag.assert_called_once_with(dest_tag)

          unique_path_mock.assert_called_once_with('cli-build')
@@ -702,14 +705,15 @@ 

              'random_path/' + source, target, opts, priority=priority)

          self.session.logout.assert_called_once()

          watch_tasks_mock.assert_called_once_with(

-             self.session, [task_id], quiet=quiet)

+             self.session, [task_id], quiet=quiet,

+             poll_interval=self.options.poll_interval)

          self.assertEqual(rv, 0)

  

      @mock.patch('sys.stdout', new_callable=six.StringIO)

-     @mock.patch('koji_cli.activate_session')

-     @mock.patch('koji_cli._unique_path', return_value='random_path')

-     @mock.patch('koji_cli._running_in_bg', return_value=False)

-     @mock.patch('koji_cli.watch_tasks', return_value=0)

+     @mock.patch('koji_cli.commands.activate_session')

+     @mock.patch('koji_cli.commands._unique_path', return_value='random_path')

+     @mock.patch('koji_cli.commands._running_in_bg', return_value=False)

+     @mock.patch('koji_cli.commands.watch_tasks', return_value=0)

      def test_handle_build_wait(

              self,

              watch_tasks_mock,
@@ -734,7 +738,7 @@ 

          # Run it and check immediate output

          # args: --wait, target, srpm

          # expected: success

-         rv = cli.handle_build(self.options, self.session, args)

+         rv = handle_build(self.options, self.session, args)

          actual = stdout.getvalue()

          expected = """Uploading srpm: srpm

  
@@ -743,26 +747,27 @@ 

  """

          self.assertMultiLineEqual(actual, expected)

          # Finally, assert that things were called as we expected.

-         activate_session_mock.assert_called_once_with(self.session)

+         activate_session_mock.assert_called_once_with(self.session, self.options)

          self.session.getBuildTarget.assert_called_once_with(target)

          self.session.getTag.assert_called_once_with(dest_tag)

          unique_path_mock.assert_called_once_with('cli-build')

          # the second one won't be executed when wait==False

          self.assertEqual(running_in_bg_mock.call_count, 1)

          self.session.uploadWrapper.assert_called_once_with(

-             source, 'random_path', callback=cli._progress_callback)

+             source, 'random_path', callback=_progress_callback)

          self.session.build.assert_called_once_with(

              'random_path/' + source, target, opts, priority=priority)

          self.session.logout.assert_called_once()

          watch_tasks_mock.assert_called_once_with(

-             self.session, [task_id], quiet=self.options.quiet)

+             self.session, [task_id], quiet=self.options.quiet,

+             poll_interval=self.options.poll_interval)

          self.assertEqual(rv, 0)

  

      @mock.patch('sys.stdout', new_callable=six.StringIO)

-     @mock.patch('koji_cli.activate_session')

-     @mock.patch('koji_cli._unique_path', return_value='random_path')

-     @mock.patch('koji_cli._running_in_bg', return_value=False)

-     @mock.patch('koji_cli.watch_tasks', return_value=0)

+     @mock.patch('koji_cli.commands.activate_session')

+     @mock.patch('koji_cli.commands._unique_path', return_value='random_path')

+     @mock.patch('koji_cli.commands._running_in_bg', return_value=False)

+     @mock.patch('koji_cli.commands.watch_tasks', return_value=0)

      def test_handle_build_nowait(

              self,

              watch_tasks_mock,
@@ -786,7 +791,7 @@ 

          # Run it and check immediate output

          # args: --nowait, target, srpm

          # expected: success

-         rv = cli.handle_build(self.options, self.session, args)

+         rv = handle_build(self.options, self.session, args)

          actual = stdout.getvalue()

          expected = """Uploading srpm: srpm

  
@@ -795,14 +800,14 @@ 

  """

          self.assertMultiLineEqual(actual, expected)

          # Finally, assert that things were called as we expected.

-         activate_session_mock.assert_called_once_with(self.session)

+         activate_session_mock.assert_called_once_with(self.session, self.options)

          self.session.getBuildTarget.assert_called_once_with(target)

          self.session.getTag.assert_called_once_with(dest_tag)

          unique_path_mock.assert_called_once_with('cli-build')

          # the second one won't be executed when wait==False

          self.assertEqual(running_in_bg_mock.call_count, 1)

          self.session.uploadWrapper.assert_called_once_with(

-             source, 'random_path', callback=cli._progress_callback)

+             source, 'random_path', callback=_progress_callback)

          self.session.build.assert_called_once_with(

              'random_path/' + source, target, opts, priority=priority)

          self.session.logout.assert_not_called()

@@ -1,15 +1,12 @@ 

  from __future__ import absolute_import

- import unittest

  

- import os

- import sys

  import mock

+ import os

  import six

+ import sys

+ import unittest

  

- from . import loadcli

- 

- cli = loadcli.cli

- 

+ from koji_cli.commands import handle_chain_build

  

  class TestChainBuild(unittest.TestCase):

      # Show long diffs in error output...
@@ -20,13 +17,14 @@ 

          self.options = mock.MagicMock()

          self.options.quiet = None

          self.options.weburl = 'weburl'

+         self.options.poll_interval = 0

          # Mock out the xmlrpc server

          self.session = mock.MagicMock()

  

      @mock.patch('sys.stdout', new_callable=six.StringIO)

-     @mock.patch('koji_cli.activate_session')

-     @mock.patch('koji_cli._running_in_bg', return_value=False)

-     @mock.patch('koji_cli.watch_tasks', return_value=0)

+     @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)

      def test_handle_chain_build(self, watch_tasks_mock, running_in_bg_mock,

                                  activate_session_mock, stdout):

          target = 'target'
@@ -63,14 +61,14 @@ 

          # Run it and check immediate output

          # args: target http://scm1 : http://scm2 http://scm3 n-v-r-1 : n-v-r-2 n-v-r-3

          # expected: success

-         rv = cli.handle_chain_build(self.options, self.session, args)

+         rv = handle_chain_build(self.options, self.session, args)

          actual = stdout.getvalue()

          expected = """Created task: 1

  Task info: weburl/taskinfo?taskID=1

  """

          self.assertMultiLineEqual(actual, expected)

          # Finally, assert that things were called as we expected.

-         activate_session_mock.assert_called_once_with(self.session)

+         activate_session_mock.assert_called_once_with(self.session, self.options)

          self.session.getBuildTarget.assert_called_once_with(target)

          self.session.getTag.assert_called_once_with(dest_tag_id, strict=True)

          self.session.getFullInheritance.assert_called_once_with(build_tag_id)
@@ -79,14 +77,15 @@ 

          running_in_bg_mock.assert_called_once()

          self.session.logout.assert_called()

          watch_tasks_mock.assert_called_once_with(

-             self.session, [task_id], quiet=self.options.quiet)

+             self.session, [task_id], quiet=self.options.quiet,

+             poll_interval=self.options.poll_interval)

          self.assertEqual(rv, 0)

  

      @mock.patch('sys.stdout', new_callable=six.StringIO)

      @mock.patch('sys.stderr', new_callable=six.StringIO)

-     @mock.patch('koji_cli.activate_session')

-     @mock.patch('koji_cli._running_in_bg', return_value=False)

-     @mock.patch('koji_cli.watch_tasks', return_value=0)

+     @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)

      def test_handle_chain_build_no_arg(

              self,

              watch_tasks_mock,
@@ -99,7 +98,7 @@ 

  

          # Run it and check immediate output

          with self.assertRaises(SystemExit) as cm:

-             cli.handle_chain_build(self.options, self.session, args)

+             handle_chain_build(self.options, self.session, args)

          actual_stdout = stdout.getvalue()

          actual_stderr = stderr.getvalue()

          expected_stdout = ''
@@ -124,9 +123,9 @@ 

  

      @mock.patch('sys.stdout', new_callable=six.StringIO)

      @mock.patch('sys.stderr', new_callable=six.StringIO)

-     @mock.patch('koji_cli.activate_session')

-     @mock.patch('koji_cli._running_in_bg', return_value=False)

-     @mock.patch('koji_cli.watch_tasks', return_value=0)

+     @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)

      def test_handle_chain_build_help(

              self,

              watch_tasks_mock,
@@ -139,7 +138,7 @@ 

  

          # Run it and check immediate output

          with self.assertRaises(SystemExit) as cm:

-             cli.handle_chain_build(self.options, self.session, args)

+             handle_chain_build(self.options, self.session, args)

          actual_stdout = stdout.getvalue()

          actual_stderr = stderr.getvalue()

          expected_stdout = """Usage: %s chain-build [options] target URL [URL2 [:] URL3 [:] URL4 ...]
@@ -167,9 +166,9 @@ 

          self.assertEqual(cm.exception.code, 0)

  

      @mock.patch('sys.stderr', new_callable=six.StringIO)

-     @mock.patch('koji_cli.activate_session')

-     @mock.patch('koji_cli._running_in_bg', return_value=False)

-     @mock.patch('koji_cli.watch_tasks', return_value=0)

+     @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)

      def test_handle_chain_build_target_not_found(

              self,

              watch_tasks_mock,
@@ -196,7 +195,7 @@ 

          # args: target http://scm1 : http://scm2 http://scm3 n-v-r-1 : n-v-r-2 n-v-r-3

          # expected: failed, target not found

          with self.assertRaises(SystemExit) as cm:

-             cli.handle_chain_build(self.options, self.session, args)

+             handle_chain_build(self.options, self.session, args)

          actual = stderr.getvalue()

          expected = """Usage: %s chain-build [options] target URL [URL2 [:] URL3 [:] URL4 ...]

  (Specify the --help global option for a list of other help options)
@@ -205,7 +204,7 @@ 

  """ % (progname, progname)

          self.assertMultiLineEqual(actual, expected)

          # Finally, assert that things were called as we expected.

-         activate_session_mock.assert_called_once_with(self.session)

+         activate_session_mock.assert_called_once_with(self.session, self.options)

          self.session.getBuildTarget.assert_called_once_with(target)

          self.session.getTag.assert_not_called()

          self.session.getFullInheritance.assert_not_called()
@@ -216,9 +215,9 @@ 

          self.assertEqual(cm.exception.code, 2)

  

      @mock.patch('sys.stderr', new_callable=six.StringIO)

-     @mock.patch('koji_cli.activate_session')

-     @mock.patch('koji_cli._running_in_bg', return_value=False)

-     @mock.patch('koji_cli.watch_tasks', return_value=0)

+     @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)

      def test_handle_build_dest_tag_locked(

              self,

              watch_tasks_mock,
@@ -255,7 +254,7 @@ 

          # args: target http://scm1 : http://scm2 http://scm3 n-v-r-1 : n-v-r-2 n-v-r-3

          # expected: failed, dest_tag is locked

          with self.assertRaises(SystemExit) as cm:

-             cli.handle_chain_build(self.options, self.session, args)

+             handle_chain_build(self.options, self.session, args)

          actual = stderr.getvalue()

          expected = """Usage: %s chain-build [options] target URL [URL2 [:] URL3 [:] URL4 ...]

  (Specify the --help global option for a list of other help options)
@@ -264,7 +263,7 @@ 

  """ % (progname, progname)

          self.assertMultiLineEqual(actual, expected)

          # Finally, assert that things were called as we expected.

-         activate_session_mock.assert_called_once_with(self.session)

+         activate_session_mock.assert_called_once_with(self.session, self.options)

          self.session.getBuildTarget.assert_called_once_with(target)

          self.session.getTag.assert_called_once_with(dest_tag_id, strict=True)

          self.session.getFullInheritance.assert_not_called()
@@ -275,9 +274,9 @@ 

          self.assertEqual(cm.exception.code, 2)

  

      @mock.patch('sys.stdout', new_callable=six.StringIO)

-     @mock.patch('koji_cli.activate_session')

-     @mock.patch('koji_cli._running_in_bg', return_value=False)

-     @mock.patch('koji_cli.watch_tasks', return_value=0)

+     @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)

      def test_handle_build_dest_tag_not_inherited_by_build_tag(

              self, watch_tasks_mock, running_in_bg_mock, activate_session_mock, stdout):

          target = 'target'
@@ -310,14 +309,14 @@ 

          # Run it and check immediate output

          # args: target, target http://scm1 : http://scm2 http://scm3 n-v-r-1 : n-v-r-2 n-v-r-3

          # expected: failed, dest_tag is not in build_tag's inheritance

-         rv = cli.handle_chain_build(self.options, self.session, args)

+         rv = handle_chain_build(self.options, self.session, args)

          actual = stdout.getvalue()

          expected = """Packages in destination tag dest_tag are not inherited by build tag build_tag

  Target target is not usable for a chain-build

  """

          self.assertMultiLineEqual(actual, expected)

          # Finally, assert that things were called as we expected.

-         activate_session_mock.assert_called_once_with(self.session)

+         activate_session_mock.assert_called_once_with(self.session, self.options)

          self.session.getBuildTarget.assert_called_once_with(target)

          self.session.getTag.assert_called_once_with(dest_tag_id, strict=True)

          self.session.getFullInheritance.assert_called_once_with(build_tag_id)
@@ -327,9 +326,9 @@ 

          watch_tasks_mock.assert_not_called()

          self.assertEqual(rv, 1)

  

-     @mock.patch('koji_cli.activate_session')

-     @mock.patch('koji_cli._running_in_bg', return_value=False)

-     @mock.patch('koji_cli.watch_tasks', return_value=0)

+     @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)

      def test_handle_chain_build_invalidated_src(

              self,

              watch_tasks_mock,
@@ -365,12 +364,12 @@ 

              # Run it and check immediate output

              # args: target badnvr : http://scm2 http://scm3 n-v-r-1 : n-v-r-2 n-v-r-3

              # expected: failed, src is neither scm nor good n-v-r

-             rv = cli.handle_chain_build(self.options, self.session, args)

+             rv = handle_chain_build(self.options, self.session, args)

              actual = stdout.getvalue()

              expected = '"badnvr" is not a SCM URL or package N-V-R\n'

              self.assertMultiLineEqual(actual, expected)

              # Finally, assert that things were called as we expected.

-             activate_session_mock.assert_called_once_with(self.session)

+             activate_session_mock.assert_called_once_with(self.session, self.options)

              self.session.getBuildTarget.assert_called_once_with(target)

              self.session.getTag.assert_called_once_with(

                  dest_tag_id, strict=True)
@@ -395,7 +394,7 @@ 

              args = [target] + source_args

              # args: target path/n-v-r : http://scm2 http://scm3 n-v-r-1 : n-v-r-2 n-v-r-3

              # expected: failed

-             cli.handle_chain_build(self.options, self.session, args)

+             handle_chain_build(self.options, self.session, args)

              actual = stdout.getvalue()

              expected = '"path/n-v-r" is not a SCM URL or package N-V-R\n'

              self.assertMultiLineEqual(actual, expected)
@@ -413,7 +412,7 @@ 

              args = [target] + source_args

              # args: target badn-vr : http://scm2 http://scm3 n-v-r-1 : n-v-r-2 n-v-r-3

              # expected: failed

-             cli.handle_chain_build(self.options, self.session, args)

+             handle_chain_build(self.options, self.session, args)

              actual = stdout.getvalue()

              expected = '"badn-vr" is not a SCM URL or package N-V-R\n'

              self.assertMultiLineEqual(actual, expected)
@@ -431,7 +430,7 @@ 

              args = [target] + source_args

              # args: target badn-v-r.rpm : http://scm2 http://scm3 n-v-r-1 : n-v-r-2 n-v-r-3

              # expected: failed

-             cli.handle_chain_build(self.options, self.session, args)

+             handle_chain_build(self.options, self.session, args)

              actual = stdout.getvalue()

              expected = '"badn-v-r.rpm" is not a SCM URL or package N-V-R\n'

              self.assertMultiLineEqual(actual, expected)
@@ -445,7 +444,7 @@ 

              # args: target http://scm

              # expected: failed, only one src found

              with self.assertRaises(SystemExit) as cm:

-                 cli.handle_chain_build(self.options, self.session, args)

+                 handle_chain_build(self.options, self.session, args)

              actual = stderr.getvalue()

              expected = """Usage: %s chain-build [options] target URL [URL2 [:] URL3 [:] URL4 ...]

  (Specify the --help global option for a list of other help options)
@@ -457,9 +456,9 @@ 

              self.assertEqual(cm.exception.code, 2)

  

      @mock.patch('sys.stdout', new_callable=six.StringIO)

-     @mock.patch('koji_cli.activate_session')

-     @mock.patch('koji_cli._running_in_bg', return_value=False)

-     @mock.patch('koji_cli.watch_tasks', return_value=0)

+     @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)

      def test_handle_chain_build_background(

              self,

              watch_tasks_mock,
@@ -500,14 +499,14 @@ 

          # Run it and check immediate output

          # args: target http://scm1 : http://scm2 http://scm3 n-v-r-1 : n-v-r-2 n-v-r-3

          # expected: success

-         rv = cli.handle_chain_build(self.options, self.session, args)

+         rv = handle_chain_build(self.options, self.session, args)

          actual = stdout.getvalue()

          expected = """Created task: 1

  Task info: weburl/taskinfo?taskID=1

  """

          self.assertMultiLineEqual(actual, expected)

          # Finally, assert that things were called as we expected.

-         activate_session_mock.assert_called_once_with(self.session)

+         activate_session_mock.assert_called_once_with(self.session, self.options)

          self.session.getBuildTarget.assert_called_once_with(target)

          self.session.getTag.assert_called_once_with(dest_tag_id, strict=True)

          self.session.getFullInheritance.assert_called_once_with(build_tag_id)
@@ -516,13 +515,14 @@ 

          running_in_bg_mock.assert_called_once()

          self.session.logout.assert_called()

          watch_tasks_mock.assert_called_once_with(

-             self.session, [task_id], quiet=self.options.quiet)

+             self.session, [task_id], quiet=self.options.quiet,

+             poll_interval=self.options.poll_interval)

          self.assertEqual(rv, 0)

  

      @mock.patch('sys.stdout', new_callable=six.StringIO)

-     @mock.patch('koji_cli.activate_session')

-     @mock.patch('koji_cli._running_in_bg', return_value=False)

-     @mock.patch('koji_cli.watch_tasks', return_value=0)

+     @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)

      def test_handle_chain_build_quiet(

              self,

              watch_tasks_mock,
@@ -564,12 +564,12 @@ 

          # Run it and check immediate output

          # args: target http://scm1 : http://scm2 http://scm3 n-v-r-1 : n-v-r-2 n-v-r-3

          # expected: success

-         rv = cli.handle_chain_build(self.options, self.session, args)

+         rv = handle_chain_build(self.options, self.session, args)

          actual = stdout.getvalue()

          expected = ''

          self.assertMultiLineEqual(actual, expected)

          # Finally, assert that things were called as we expected.

-         activate_session_mock.assert_called_once_with(self.session)

+         activate_session_mock.assert_called_once_with(self.session, self.options)

          self.session.getBuildTarget.assert_called_once_with(target)

          self.session.getTag.assert_called_once_with(dest_tag_id, strict=True)

          self.session.getFullInheritance.assert_called_once_with(build_tag_id)
@@ -578,13 +578,14 @@ 

          running_in_bg_mock.assert_called_once()

          self.session.logout.assert_called()

          watch_tasks_mock.assert_called_once_with(

-             self.session, [task_id], quiet=self.options.quiet)

+             self.session, [task_id], quiet=self.options.quiet,

+             poll_interval=self.options.poll_interval)

          self.assertEqual(rv, 0)

  

      @mock.patch('sys.stdout', new_callable=six.StringIO)

-     @mock.patch('koji_cli.activate_session')

-     @mock.patch('koji_cli._running_in_bg', return_value=True)

-     @mock.patch('koji_cli.watch_tasks', return_value=0)

+     @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)

      def test_handle_chain_build_running_in_bg(

              self,

              watch_tasks_mock,
@@ -625,14 +626,14 @@ 

          # Run it and check immediate output

          # args: target http://scm1 : http://scm2 http://scm3 n-v-r-1 : n-v-r-2 n-v-r-3

          # expected: success

-         rv = cli.handle_chain_build(self.options, self.session, args)

+         rv = handle_chain_build(self.options, self.session, args)

          actual = stdout.getvalue()

          expected = """Created task: 1

  Task info: weburl/taskinfo?taskID=1

  """

          self.assertMultiLineEqual(actual, expected)

          # Finally, assert that things were called as we expected.

-         activate_session_mock.assert_called_once_with(self.session)

+         activate_session_mock.assert_called_once_with(self.session, self.options)

          self.session.getBuildTarget.assert_called_once_with(target)

          self.session.getTag.assert_called_once_with(dest_tag_id, strict=True)

          self.session.getFullInheritance.assert_called_once_with(build_tag_id)
@@ -644,9 +645,9 @@ 

          self.assertIsNone(rv)

  

      @mock.patch('sys.stdout', new_callable=six.StringIO)

-     @mock.patch('koji_cli.activate_session')

-     @mock.patch('koji_cli._running_in_bg', return_value=False)

-     @mock.patch('koji_cli.watch_tasks', return_value=0)

+     @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)

      def test_handle_chain_build_nowait(

              self,

              watch_tasks_mock,
@@ -687,14 +688,14 @@ 

          # Run it and check immediate output

          # args: target http://scm1 : http://scm2 http://scm3 n-v-r-1 : n-v-r-2 n-v-r-3

          # expected: success

-         rv = cli.handle_chain_build(self.options, self.session, args)

+         rv = handle_chain_build(self.options, self.session, args)

          actual = stdout.getvalue()

          expected = """Created task: 1

  Task info: weburl/taskinfo?taskID=1

  """

          self.assertMultiLineEqual(actual, expected)

          # Finally, assert that things were called as we expected.

-         activate_session_mock.assert_called_once_with(self.session)

+         activate_session_mock.assert_called_once_with(self.session, self.options)

          self.session.getBuildTarget.assert_called_once_with(target)

          self.session.getTag.assert_called_once_with(dest_tag_id, strict=True)

          self.session.getFullInheritance.assert_called_once_with(build_tag_id)

@@ -1,16 +1,12 @@ 

  from __future__ import absolute_import

- import unittest

- 

- import os

- import sys

  import mock

+ import os

  import six

+ import sys

+ import unittest

  from mock import call

  

- from . import loadcli

- 

- cli = loadcli.cli

- 

+ from koji_cli.commands import handle_edit_host

  

  class TestEditHost(unittest.TestCase):

  
@@ -18,7 +14,7 @@ 

      maxDiff = None

  

      @mock.patch('sys.stdout', new_callable=six.StringIO)

-     @mock.patch('koji_cli.activate_session')

+     @mock.patch('koji_cli.commands.activate_session')

      def test_handle_edit_host(self, activate_session_mock, stdout):

          host = 'host'

          host_info = mock.ANY
@@ -45,19 +41,19 @@ 

          # args: host, --arches='arch1 arch2', --capacity=0.22,

          # --description=description, --comment=comment

          # expected: success

-         rv = cli.handle_edit_host(options, session, args)

+         rv = handle_edit_host(options, session, args)

          actual = stdout.getvalue()

          expected = 'Edited host\n'

          self.assertMultiLineEqual(actual, expected)

          # Finally, assert that things were called as we expected.

-         activate_session_mock.assert_called_once_with(session)

+         activate_session_mock.assert_called_once_with(session, options)

          session.getHost.assert_called_once_with(host)

          session.editHost.assert_called_once_with(host, **kwargs)

          self.assertEqual(session.multiCall.call_count, 2)

          self.assertNotEqual(rv, 1)

  

      @mock.patch('sys.stdout', new_callable=six.StringIO)

-     @mock.patch('koji_cli.activate_session')

+     @mock.patch('koji_cli.commands.activate_session')

      def test_handle_edit_host_failed(self, activate_session_mock, stdout):

          host = 'host'

          host_info = mock.ANY
@@ -84,19 +80,19 @@ 

          # args: host, --arches='arch1 arch2', --capacity=0.22,

          # --description=description, --comment=comment

          # expected: failed - session.editHost == False

-         rv = cli.handle_edit_host(options, session, args)

+         rv = handle_edit_host(options, session, args)

          actual = stdout.getvalue()

          expected = 'No changes made to host\n'

          self.assertMultiLineEqual(actual, expected)

          # Finally, assert that things were called as we expected.

-         activate_session_mock.assert_called_once_with(session)

+         activate_session_mock.assert_called_once_with(session, options)

          session.getHost.assert_called_once_with(host)

          session.editHost.assert_called_once_with(host, **kwargs)

          self.assertEqual(session.multiCall.call_count, 2)

          self.assertNotEqual(rv, 1)

  

      @mock.patch('sys.stdout', new_callable=six.StringIO)

-     @mock.patch('koji_cli.activate_session')

+     @mock.patch('koji_cli.commands.activate_session')

      def test_handle_edit_multi_host(self, activate_session_mock, stdout):

          hosts = ['host1', 'host2']

          host_infos = [mock.ANY, mock.ANY]
@@ -124,12 +120,12 @@ 

          # args: host1, host2, --arches='arch1 arch2', --capacity=0.22,

          # --description=description, --comment=comment

          # expected: success

-         rv = cli.handle_edit_host(options, session, args)

+         rv = handle_edit_host(options, session, args)

          actual = stdout.getvalue()

          expected = 'Edited host1\nEdited host2\n'

          self.assertMultiLineEqual(actual, expected)

          # Finally, assert that things were called as we expected.

-         activate_session_mock.assert_called_once_with(session)

+         activate_session_mock.assert_called_once_with(session, options)

          self.assertEqual(session.mock_calls,

                           [call.getHost(hosts[0]),

                            call.getHost(hosts[1]),
@@ -143,7 +139,7 @@ 

  

      @mock.patch('sys.stdout', new_callable=six.StringIO)

      @mock.patch('sys.stderr', new_callable=six.StringIO)

-     @mock.patch('koji_cli.activate_session')

+     @mock.patch('koji_cli.commands.activate_session')

      def test_handle_edit_host_no_arg(

              self, activate_session_mock, stderr, stdout):

          args = []
@@ -158,7 +154,7 @@ 

          # args: _empty_

          # expected: failed - should specify host

          with self.assertRaises(SystemExit) as cm:

-             cli.handle_edit_host(options, session, args)

+             handle_edit_host(options, session, args)

          actual_stdout = stdout.getvalue()

          actual_stderr = stderr.getvalue()

          expected_stdout = ''
@@ -178,7 +174,7 @@ 

          self.assertEqual(cm.exception.code, 2)

  

      @mock.patch('sys.stdout', new_callable=six.StringIO)

-     @mock.patch('koji_cli.activate_session')

+     @mock.patch('koji_cli.commands.activate_session')

      def test_handle_edit_host_no_host(self, activate_session_mock, stdout):

          host = 'host'

          host_info = None
@@ -201,14 +197,14 @@ 

          # args: host, --arches='arch1 arch2', --capacity=0.22,

          # --description=description, --comment=comment

          # expected: failed -- getHost() == None

-         rv = cli.handle_edit_host(options, session, args)

+         rv = handle_edit_host(options, session, args)

          actual = stdout.getvalue()

          expected = """Host host does not exist

  No changes made, please correct the command line

  """

          self.assertMultiLineEqual(actual, expected)

          # Finally, assert that things were called as we expected.

-         activate_session_mock.assert_called_once_with(session)

+         activate_session_mock.assert_called_once_with(session, options)

          session.getHost.assert_called_once_with(host)

          session.editHost.assert_not_called()

          self.assertEqual(session.multiCall.call_count, 1)

file modified
+13 -16
@@ -1,15 +1,12 @@ 

  from __future__ import absolute_import

- import unittest

- 

- import os

- import sys

  import mock

+ import os

  import six

+ import sys

+ import unittest

  

  

- from . import loadcli

- 

- cli = loadcli.cli

+ from koji_cli.commands import handle_edit_tag

  

  progname = os.path.basename(sys.argv[0]) or 'koji'

  
@@ -19,7 +16,7 @@ 

      maxDiff = None

  

      @mock.patch('sys.stdout', new_callable=six.StringIO)

-     @mock.patch('koji_cli.activate_session')

+     @mock.patch('koji_cli.commands.activate_session')

      def test_handle_edit_tag(self, activate_session_mock, stdout):

          tag = 'tag'

          arches = 'arch1 arch2'
@@ -61,12 +58,12 @@ 

          # --rename=tag2 --maven-support --include-all

          # -x extraA=A -x extraB=True -r extraC -r extraD

          # expected: success

-         rv = cli.handle_edit_tag(options, session, args)

+         rv = handle_edit_tag(options, session, args)

          actual = stdout.getvalue()

          expected = ''

          self.assertMultiLineEqual(actual, expected)

          # Finally, assert that things were called as we expected.

-         activate_session_mock.assert_called_once_with(session)

+         activate_session_mock.assert_called_once_with(session, options)

          session.editTag2.assert_called_once_with(tag, **opts)

          self.assertEqual(rv, None)

  
@@ -86,18 +83,18 @@ 

          # Run it and check immediate output

          # args: tag --no-perm --unlock --no-maven-support --no-include-all

          # expected: success

-         rv = cli.handle_edit_tag(options, session, args)

+         rv = handle_edit_tag(options, session, args)

          actual = stdout.getvalue()

          expected = ''

          self.assertMultiLineEqual(actual, expected)

          # Finally, assert that things were called as we expected.

-         activate_session_mock.assert_called_once_with(session)

+         activate_session_mock.assert_called_once_with(session, options)

          session.editTag2.assert_called_once_with(tag, **opts)

          self.assertEqual(rv, None)

  

      @mock.patch('sys.stdout', new_callable=six.StringIO)

      @mock.patch('sys.stderr', new_callable=six.StringIO)

-     @mock.patch('koji_cli.activate_session')

+     @mock.patch('koji_cli.commands.activate_session')

      def test_handle_edit_tag_help(self, activate_session_mock, stderr, stdout):

          args = ['--help']

          options = mock.MagicMock()
@@ -109,7 +106,7 @@ 

          # args: --help

          # expected: failed, help info shows

          with self.assertRaises(SystemExit) as cm:

-             cli.handle_edit_tag(options, session, args)

+             handle_edit_tag(options, session, args)

          actual_stdout = stdout.getvalue()

          actual_stderr = stderr.getvalue()

          expected_stdout = """Usage: %s edit-tag [options] name
@@ -144,7 +141,7 @@ 

  

      @mock.patch('sys.stdout', new_callable=six.StringIO)

      @mock.patch('sys.stderr', new_callable=six.StringIO)

-     @mock.patch('koji_cli.activate_session')

+     @mock.patch('koji_cli.commands.activate_session')

      def test_handle_edit_tag_no_arg(self, activate_session_mock, stderr, stdout):

          args = []

          options = mock.MagicMock()
@@ -156,7 +153,7 @@ 

          # args: --help

          # expected: failed, help info shows

          with self.assertRaises(SystemExit) as cm:

-             cli.handle_edit_tag(options, session, args)

+             handle_edit_tag(options, session, args)

          actual_stdout = stdout.getvalue()

          actual_stderr = stderr.getvalue()

          expected_stdout = ''

@@ -1,11 +1,10 @@ 

  from __future__ import absolute_import

  import json

- import unittest

- import os

- import sys

  import mock

+ import os

  import six

- from . import loadcli

+ import sys

+ import unittest

  

  try:

      import libcomps
@@ -16,18 +15,19 @@ 

  except ImportError:

      yumcomps = None

  

- cli = loadcli.cli

- 

+ import koji_cli.commands

+ from koji_cli.commands import handle_import_comps, _import_comps,\

+                               _import_comps_alt

  

  class TestImportComps(unittest.TestCase):

      # Show long diffs in error output...

      maxDiff = None

  

      @mock.patch('sys.stdout', new_callable=six.StringIO)

-     @mock.patch('koji_cli.libcomps')

-     @mock.patch('koji_cli.activate_session')

-     @mock.patch('koji_cli._import_comps')

-     @mock.patch('koji_cli._import_comps_alt')

+     @mock.patch('koji_cli.commands.libcomps')

+     @mock.patch('koji_cli.commands.activate_session')

+     @mock.patch('koji_cli.commands._import_comps')

+     @mock.patch('koji_cli.commands._import_comps_alt')

      def test_handle_import_comps_libcomps(

              self,

              mock_import_comps_alt,
@@ -50,13 +50,13 @@ 

          # Run it and check immediate output

          # args: ./data/comps-example.xml, tag

          # expected: success

-         rv = cli.handle_import_comps(options, session, args)

+         rv = handle_import_comps(options, session, args)

          actual = stdout.getvalue()

          expected = ''

          self.assertMultiLineEqual(actual, expected)

  

          # Finally, assert that things were called as we expected.

-         mock_activate_session.assert_called_once_with(session)

+         mock_activate_session.assert_called_once_with(session, options)

          session.getTag.assert_called_once_with(tag)

          mock_import_comps.assert_called_once_with(

              session, filename, tag, kwargs)
@@ -64,11 +64,11 @@ 

          self.assertNotEqual(rv, 1)

  

      @mock.patch('sys.stdout', new_callable=six.StringIO)

-     @mock.patch('koji_cli.libcomps', new=None)

-     @mock.patch('koji_cli.yumcomps', create=True)

-     @mock.patch('koji_cli.activate_session')

-     @mock.patch('koji_cli._import_comps')

-     @mock.patch('koji_cli._import_comps_alt')

+     @mock.patch('koji_cli.commands.libcomps', new=None)

+     @mock.patch('koji_cli.commands.yumcomps', create=True)

+     @mock.patch('koji_cli.commands.activate_session')

+     @mock.patch('koji_cli.commands._import_comps')

+     @mock.patch('koji_cli.commands._import_comps_alt')

      def test_handle_import_comps_yumcomps(

              self,

              mock_import_comps_alt,
@@ -91,13 +91,13 @@ 

          # Run it and check immediate output

          # args: --force, ./data/comps-example.xml, tag

          # expected: success

-         rv = cli.handle_import_comps(options, session, args)

+         rv = handle_import_comps(options, session, args)

          actual = stdout.getvalue()

          expected = ''

          self.assertMultiLineEqual(actual, expected)

  

          # Finally, assert that things were called as we expected.

-         mock_activate_session.assert_called_once_with(session)

+         mock_activate_session.assert_called_once_with(session, options)

          session.getTag.assert_called_once_with(tag)

          mock_import_comps.assert_not_called()

          mock_import_comps_alt.assert_called_once_with(
@@ -105,11 +105,11 @@ 

          self.assertNotEqual(rv, 1)

  

      @mock.patch('sys.stdout', new_callable=six.StringIO)

-     @mock.patch('koji_cli.libcomps', new=None)

-     @mock.patch('koji_cli.yumcomps', new=None, create=True)

-     @mock.patch('koji_cli.activate_session')

-     @mock.patch('koji_cli._import_comps')

-     @mock.patch('koji_cli._import_comps_alt')

+     @mock.patch('koji_cli.commands.libcomps', new=None)

+     @mock.patch('koji_cli.commands.yumcomps', new=None, create=True)

+     @mock.patch('koji_cli.commands.activate_session')

+     @mock.patch('koji_cli.commands._import_comps')

+     @mock.patch('koji_cli.commands._import_comps_alt')

      def test_handle_import_comps_comps_na(

              self,

              mock_import_comps_alt,
@@ -129,22 +129,22 @@ 

          # Run it and check immediate output

          # args: --force, ./data/comps-example.xml, tag

          # expected: failed, no comps available

-         rv = cli.handle_import_comps(options, session, args)

+         rv = handle_import_comps(options, session, args)

          actual = stdout.getvalue()

          expected = 'comps module not available\n'

          self.assertMultiLineEqual(actual, expected)

  

          # Finally, assert that things were called as we expected.

-         mock_activate_session.assert_called_once_with(session)

+         mock_activate_session.assert_called_once_with(session, options)

          session.getTag.assert_called_once_with(tag)

          mock_import_comps.assert_not_called()

          mock_import_comps_alt.assert_not_called()

          self.assertEqual(rv, 1)

  

      @mock.patch('sys.stdout', new_callable=six.StringIO)

-     @mock.patch('koji_cli.activate_session')

-     @mock.patch('koji_cli._import_comps')

-     @mock.patch('koji_cli._import_comps_alt')

+     @mock.patch('koji_cli.commands.activate_session')

+     @mock.patch('koji_cli.commands._import_comps')

+     @mock.patch('koji_cli.commands._import_comps_alt')

      def test_handle_import_comps_tag_not_exists(

              self,

              mock_import_comps_alt,
@@ -164,13 +164,13 @@ 

          # Run it and check immediate output

          # args: ./data/comps-example.xml, tag

          # expected: failed: tag does not exist

-         rv = cli.handle_import_comps(options, session, args)

+         rv = handle_import_comps(options, session, args)

          actual = stdout.getvalue()

          expected = 'No such tag: tag\n'

          self.assertMultiLineEqual(actual, expected)

  

          # Finally, assert that things were called as we expected.

-         mock_activate_session.assert_called_once_with(session)

+         mock_activate_session.assert_called_once_with(session, options)

          session.getTag.assert_called_once_with(tag)

          mock_import_comps.assert_not_called()

          mock_import_comps_alt.assert_not_called()
@@ -178,9 +178,9 @@ 

  

      @mock.patch('sys.stdout', new_callable=six.StringIO)

      @mock.patch('sys.stderr', new_callable=six.StringIO)

-     @mock.patch('koji_cli.activate_session')

-     @mock.patch('koji_cli._import_comps')

-     @mock.patch('koji_cli._import_comps_alt')

+     @mock.patch('koji_cli.commands.activate_session')

+     @mock.patch('koji_cli.commands._import_comps')

+     @mock.patch('koji_cli.commands._import_comps_alt')

      def test_handle_import_comps_help(

              self,

              mock_import_comps_alt, mock_import_comps,
@@ -196,7 +196,7 @@ 

  

          # Run it and check immediate output

          with self.assertRaises(SystemExit) as cm:

-             rv = cli.handle_import_comps(options, session, args)

+             rv = handle_import_comps(options, session, args)

          actual_stdout = stdout.getvalue()

          actual_stderr = stderr.getvalue()

          expected_stdout = ''
@@ -224,7 +224,7 @@ 

          calls_file = os.path.dirname(

              __file__) + '/data/comps-example.libcomps.calls'

          self._test_import_comps(

-             cli._import_comps,

+             _import_comps,

              comps_file,

              stdout_file,

              calls_file,
@@ -239,7 +239,7 @@ 

          calls_file = os.path.dirname(

              __file__) + '/data/comps-sample.libcomps.calls'

          self._test_import_comps(

-             cli._import_comps,

+             _import_comps,

              comps_file,

              stdout_file,

              calls_file,
@@ -247,8 +247,8 @@ 

  

      @unittest.skipIf(yumcomps is None, "No yum.comps")

      @mock.patch('sys.stdout', new_callable=six.StringIO)

-     @mock.patch('koji_cli.libcomps', new=None)

-     @mock.patch('koji_cli.yumcomps', create=True, new=yumcomps)

+     @mock.patch('koji_cli.commands.libcomps', new=None)

+     @mock.patch('koji_cli.commands.yumcomps', create=True, new=yumcomps)

      def test_import_comps_yumcomps(self, stdout):

          comps_file = os.path.dirname(__file__) + '/data/comps-example.xml'

          stdout_file = os.path.dirname(
@@ -256,7 +256,7 @@ 

          calls_file = os.path.dirname(

              __file__) + '/data/comps-example.yumcomps.calls'

          self._test_import_comps(

-             cli._import_comps_alt,

+             _import_comps_alt,

              comps_file,

              stdout_file,

              calls_file,
@@ -264,8 +264,8 @@ 

  

      @unittest.skipIf(yumcomps is None, "No yum.comps")

      @mock.patch('sys.stdout', new_callable=six.StringIO)

-     @mock.patch('koji_cli.libcomps', new=None)

-     @mock.patch('koji_cli.yumcomps', create=True, new=yumcomps)

+     @mock.patch('koji_cli.commands.libcomps', new=None)

+     @mock.patch('koji_cli.commands.yumcomps', create=True, new=yumcomps)

      def test_import_comps_sample_yumcomps(self, stdout):

          comps_file = os.path.dirname(__file__) + '/data/comps-sample.xml'

          stdout_file = os.path.dirname(
@@ -273,7 +273,7 @@ 

          calls_file = os.path.dirname(

              __file__) + '/data/comps-sample.yumcomps.calls'

          self._test_import_comps(

-             cli._import_comps_alt,

+             _import_comps_alt,

              comps_file,

              stdout_file,

              calls_file,
@@ -342,20 +342,20 @@ 

      comps_file = path + '/data/comps-example.xml'

      stdout_file = path + '/data/comps-example.libcomps.out'

      calls_file = path + '/data/comps-example.libcomps.calls'

-     _generate_out_calls(cli._import_comps, comps_file, stdout_file, calls_file)

+     _generate_out_calls(_import_comps, comps_file, stdout_file, calls_file)

  

      comps_file = path + '/data/comps-sample.xml'

      stdout_file = path + '/data/comps-sample.libcomps.out'

      calls_file = path + '/data/comps-sample.libcomps.calls'

-     _generate_out_calls(cli._import_comps, comps_file, stdout_file, calls_file)

+     _generate_out_calls(_import_comps, comps_file, stdout_file, calls_file)

  

-     cli.yumcomps = yumcomps

+     koji_cli.commands.yumcomps = yumcomps

  

      comps_file = path + '/data/comps-example.xml'

      stdout_file = path + '/data/comps-example.yumcomps.out'

      calls_file = path + '/data/comps-example.yumcomps.calls'

      _generate_out_calls(

-         cli._import_comps_alt,

+         _import_comps_alt,

          comps_file,

          stdout_file,

          calls_file)
@@ -364,7 +364,7 @@ 

      stdout_file = path + '/data/comps-sample.yumcomps.out'

      calls_file = path + '/data/comps-sample.yumcomps.calls'

      _generate_out_calls(

-         cli._import_comps_alt,

+         _import_comps_alt,

          comps_file,

          stdout_file,

          calls_file)

@@ -5,37 +5,28 @@ 

  

  import koji

  

- from . import loadcli

- cli = loadcli.cli

+ from koji_cli.commands import anon_handle_list_channels

  

  class TestListChannels(unittest.TestCase):

      def setUp(self):

          self.options = mock.MagicMock()

+         self.options.quiet = True

          self.session = mock.MagicMock()

          self.session.getAPIVersion.return_value = koji.API_VERSION

-         self.args = mock.MagicMock()

-         self.original_parser = cli.OptionParser

-         cli.OptionParser = mock.MagicMock()

-         self.parser = cli.OptionParser.return_value

-         cli.options = self.options  # globals!!!

- 

-     def tearDown(self):

-         cli.OptionParser = self.original_parser

+         self.args = []

  

      @mock.patch('sys.stdout', new_callable=StringIO)

-     def test_list_channels(self, stdout):

-         options = mock.MagicMock()

-         options.quiet = True

-         self.parser.parse_args.return_value = [options, []]

- 

-         # mock xmlrpc

+     @mock.patch('koji_cli.commands.activate_session')

+     def test_list_channels(self, activate_session_mock, stdout):

          self.session.listChannels.return_value = [

              {'id': 1, 'name': 'default'},

              {'id': 2, 'name': 'test'},

          ]

          self.session.multiCall.return_value = [[[1,2,3]], [[4,5]]]

  

-         cli.anon_handle_list_channels(self.options, self.session, self.args)

+         anon_handle_list_channels(self.options, self.session, self.args)

+ 

          actual = stdout.getvalue()

-         expected = 'successfully connected to hub\ndefault             3\ntest                2\n'

+         expected = 'default             3\ntest                2\n'

          self.assertMultiLineEqual(actual, expected)

+         activate_session_mock.assert_called_once_with(self.session, self.options)

@@ -1,8 +1,8 @@ 

  from __future__ import absolute_import

  import mock

  import os

- import unittest

  import six

+ import unittest

  

  from . import loadcli

  cli = loadcli.cli

@@ -1,15 +1,13 @@ 

  from __future__ import absolute_import

- import unittest

  

- import os

- import sys

  import mock

- import six

- 

- from . import loadcli

  import optparse

+ import os

+ import six

+ import sys

+ import unittest

  

- cli = loadcli.cli

+ from koji_cli.commands import handle_maven_build

  

  EMPTY_BUILD_OPTS = {

      'specfile': None,
@@ -40,13 +38,14 @@ 

          self.options = mock.MagicMock()

          self.options.quiet = None

          self.options.weburl = 'weburl'

+         self.options.poll_interval = 0

          # Mock out the xmlrpc server

          self.session = mock.MagicMock()

  

      @mock.patch('sys.stdout', new_callable=six.StringIO)

-     @mock.patch('koji_cli.activate_session')

-     @mock.patch('koji_cli._running_in_bg', return_value=False)

-     @mock.patch('koji_cli.watch_tasks', return_value=0)

+     @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)

      def test_handle_maven_build(self, watch_tasks_mock, running_in_bg_mock,

                                  activate_session_mock, stdout):

          target = 'target'
@@ -66,14 +65,14 @@ 

          # Run it and check immediate output

          # args: target http://scm

          # expected: success

-         rv = cli.handle_maven_build(self.options, self.session, args)

+         rv = handle_maven_build(self.options, self.session, args)

          actual = stdout.getvalue()

          expected = """Created task: 1

  Task info: weburl/taskinfo?taskID=1

  """

          self.assertMultiLineEqual(actual, expected)

          # Finally, assert that things were called as we expected.

-         activate_session_mock.assert_called_once_with(self.session)

+         activate_session_mock.assert_called_once_with(self.session, self.options)

          self.session.getBuildTarget.assert_called_once_with(target)

          self.session.getTag.assert_called_once_with(dest_tag_id)

          self.session.mavenBuild.assert_called_once_with(
@@ -81,14 +80,15 @@ 

          running_in_bg_mock.assert_called_once()

          self.session.logout.assert_called()

          watch_tasks_mock.assert_called_once_with(

-             self.session, [task_id], quiet=self.options.quiet)

+             self.session, [task_id], quiet=self.options.quiet,

+             poll_interval=self.options.poll_interval)

          self.assertEqual(rv, 0)

  

      @mock.patch('sys.stdout', new_callable=six.StringIO)

      @mock.patch('sys.stderr', new_callable=six.StringIO)

-     @mock.patch('koji_cli.activate_session')

-     @mock.patch('koji_cli._running_in_bg', return_value=False)

-     @mock.patch('koji_cli.watch_tasks', return_value=0)

+     @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)

      def test_handle_maven_build_no_arg(

              self,

              watch_tasks_mock,
@@ -101,7 +101,7 @@ 

  

          # Run it and check immediate output

          with self.assertRaises(SystemExit) as cm:

-             cli.handle_maven_build(self.options, self.session, args)

+             handle_maven_build(self.options, self.session, args)

          actual_stdout = stdout.getvalue()

          actual_stderr = stderr.getvalue()

          expected_stdout = ''
@@ -126,9 +126,9 @@ 

  

      @mock.patch('sys.stdout', new_callable=six.StringIO)

      @mock.patch('sys.stderr', new_callable=six.StringIO)

-     @mock.patch('koji_cli.activate_session')

-     @mock.patch('koji_cli._running_in_bg', return_value=False)

-     @mock.patch('koji_cli.watch_tasks', return_value=0)

+     @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)

      def test_handle_maven_build_no_arg_with_ini(

              self,

              watch_tasks_mock,
@@ -141,7 +141,7 @@ 

  

          # Run it and check immediate output

          with self.assertRaises(SystemExit) as cm:

-             cli.handle_maven_build(self.options, self.session, args)

+             handle_maven_build(self.options, self.session, args)

          actual_stdout = stdout.getvalue()

          actual_stderr = stderr.getvalue()

          expected_stdout = ''
@@ -166,9 +166,9 @@ 

  

      @mock.patch('sys.stdout', new_callable=six.StringIO)

      @mock.patch('sys.stderr', new_callable=six.StringIO)

-     @mock.patch('koji_cli.activate_session')

-     @mock.patch('koji_cli._running_in_bg', return_value=False)

-     @mock.patch('koji_cli.watch_tasks', return_value=0)

+     @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)

      def test_handle_maven_build_help(

              self,

              watch_tasks_mock,
@@ -181,7 +181,7 @@ 

  

          # Run it and check immediate output

          with self.assertRaises(SystemExit) as cm:

-             cli.handle_maven_build(self.options, self.session, args)

+             handle_maven_build(self.options, self.session, args)

          actual_stdout = stdout.getvalue()

          actual_stderr = stderr.getvalue()

          expected_stdout = """Usage: %s maven-build [options] target URL
@@ -233,9 +233,9 @@ 

          self.assertEqual(cm.exception.code, 0)

  

      @mock.patch('sys.stderr', new_callable=six.StringIO)

-     @mock.patch('koji_cli.activate_session')

-     @mock.patch('koji_cli._running_in_bg', return_value=False)

-     @mock.patch('koji_cli.watch_tasks', return_value=0)

+     @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)

      def test_handle_maven_build_target_not_found(

              self,

              watch_tasks_mock,
@@ -254,7 +254,7 @@ 

          # args: target http://scm

          # expected: failed, target not found

          with self.assertRaises(SystemExit) as cm:

-             cli.handle_maven_build(self.options, self.session, args)

+             handle_maven_build(self.options, self.session, args)

          actual = stderr.getvalue()

          expected = """Usage: %s maven-build [options] target URL

         %s maven-build --ini=CONFIG... [options] target
@@ -264,7 +264,7 @@ 

  """ % (progname, progname, progname)

          self.assertMultiLineEqual(actual, expected)

          # Finally, assert that things were called as we expected.

-         activate_session_mock.assert_called_once_with(self.session)

+         activate_session_mock.assert_called_once_with(self.session, self.options)

          self.session.getBuildTarget.assert_called_once_with(target)

          self.session.getTag.assert_not_called()

          running_in_bg_mock.assert_not_called()
@@ -274,9 +274,9 @@ 

          self.assertEqual(cm.exception.code, 2)

  

      @mock.patch('sys.stderr', new_callable=six.StringIO)

-     @mock.patch('koji_cli.activate_session')

-     @mock.patch('koji_cli._running_in_bg', return_value=False)

-     @mock.patch('koji_cli.watch_tasks', return_value=0)

+     @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)

      def test_handle_build_dest_tag_not_found(

              self,

              watch_tasks_mock,
@@ -299,7 +299,7 @@ 

          # args: target http://scm

          # expected: failed, dest_tag not found

          with self.assertRaises(SystemExit) as cm:

-             cli.handle_maven_build(self.options, self.session, args)

+             handle_maven_build(self.options, self.session, args)

          actual = stderr.getvalue()

          expected = """Usage: %s maven-build [options] target URL

         %s maven-build --ini=CONFIG... [options] target
@@ -309,7 +309,7 @@ 

  """ % (progname, progname, progname)

          self.assertMultiLineEqual(actual, expected)

          # Finally, assert that things were called as we expected.

-         activate_session_mock.assert_called_once_with(self.session)

+         activate_session_mock.assert_called_once_with(self.session, self.options)

          self.session.getBuildTarget.assert_called_once_with(target)

          self.session.getTag.assert_called_once_with(dest_tag_id)

          running_in_bg_mock.assert_not_called()
@@ -319,9 +319,9 @@ 

          self.assertEqual(cm.exception.code, 2)

  

      @mock.patch('sys.stderr', new_callable=six.StringIO)

-     @mock.patch('koji_cli.activate_session')

-     @mock.patch('koji_cli._running_in_bg', return_value=False)

-     @mock.patch('koji_cli.watch_tasks', return_value=0)

+     @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)

      def test_handle_build_dest_tag_locked(

              self,

              watch_tasks_mock,
@@ -344,7 +344,7 @@ 

          # args: target http://scm

          # expected: failed, dest_tag is locked

          with self.assertRaises(SystemExit) as cm:

-             cli.handle_maven_build(self.options, self.session, args)

+             handle_maven_build(self.options, self.session, args)

          actual = stderr.getvalue()

          expected = """Usage: %s maven-build [options] target URL

         %s maven-build --ini=CONFIG... [options] target
@@ -354,7 +354,7 @@ 

  """ % (progname, progname, progname)

          self.assertMultiLineEqual(actual, expected)

          # Finally, assert that things were called as we expected.

-         activate_session_mock.assert_called_once_with(self.session)

+         activate_session_mock.assert_called_once_with(self.session, self.options)

          self.session.getBuildTarget.assert_called_once_with(target)

          self.session.getTag.assert_called_once_with(dest_tag_id)

          running_in_bg_mock.assert_not_called()
@@ -365,7 +365,7 @@ 

  

      @mock.patch('sys.stderr', new_callable=six.StringIO)

      @mock.patch('sys.stdout', new_callable=six.StringIO)

-     @mock.patch('koji_cli.activate_session')

+     @mock.patch('koji_cli.commands.activate_session')

      @mock.patch(

          'koji.util.parse_maven_param',

          return_value={
@@ -375,8 +375,8 @@ 

                      'pkg1',

                      'pkg2']}})

      @mock.patch('koji.util.maven_opts')

-     @mock.patch('koji_cli._running_in_bg', return_value=False)

-     @mock.patch('koji_cli.watch_tasks', return_value=0)

+     @mock.patch('koji_cli.commands._running_in_bg', return_value=False)

+     @mock.patch('koji_cli.commands.watch_tasks', return_value=0)

      def test_handle_build_inis(

              self,

              watch_tasks_mock,
@@ -415,14 +415,14 @@ 

          # Run it and check immediate output

          # args: --ini=config1.ini --ini=config2.ini --section=section target

          # expected: success

-         rv = cli.handle_maven_build(self.options, self.session, args)

+         rv = handle_maven_build(self.options, self.session, args)

          actual = stdout.getvalue()

          expected = """Created task: 1

  Task info: weburl/taskinfo?taskID=1

  """

          self.assertMultiLineEqual(actual, expected)

          # Finally, assert that things were called as we expected.

-         activate_session_mock.assert_called_once_with(self.session)

+         activate_session_mock.assert_called_once_with(self.session, self.options)

          self.session.getBuildTarget.assert_called_once_with(target)

          self.session.getTag.assert_called_once_with(dest_tag_id)

          parse_maven_param_mock.assert_called_once_with(
@@ -433,7 +433,8 @@ 

          running_in_bg_mock.assert_called_once()

          self.session.logout.assert_called_once()

          watch_tasks_mock.assert_called_once_with(

-             self.session, [task_id], quiet=build_opts.quiet)

+             self.session, [task_id], quiet=build_opts.quiet,

+             poll_interval=self.options.poll_interval)

          self.assertEqual(rv, 0)

  

          stdout.seek(0)
@@ -452,7 +453,7 @@ 

          # args: --ini=config1.ini --ini=config2.ini --section=section target

          # expected: failed, no type == 'maven' found

          with self.assertRaises(SystemExit) as cm:

-             cli.handle_maven_build(self.options, self.session, args)

+             handle_maven_build(self.options, self.session, args)

          actual = stderr.getvalue()

          expected = """Usage: %s maven-build [options] target URL

         %s maven-build --ini=CONFIG... [options] target
@@ -480,7 +481,7 @@ 

          # args: --ini=config1.ini --ini=config2.ini --section=section target

          # expected: failed, ValueError raised when parsing .ini files

          with self.assertRaises(SystemExit) as cm:

-             cli.handle_maven_build(self.options, self.session, args)

+             handle_maven_build(self.options, self.session, args)

          actual = stderr.getvalue()

          expected = """Usage: %s maven-build [options] target URL

         %s maven-build --ini=CONFIG... [options] target
@@ -497,11 +498,11 @@ 

          self.assertEqual(cm.exception.code, 2)

  

      @mock.patch('sys.stderr', new_callable=six.StringIO)

-     @mock.patch('koji_cli.activate_session')

+     @mock.patch('koji_cli.commands.activate_session')

      @mock.patch('koji.util.parse_maven_param')

      @mock.patch('koji.util.maven_opts')

-     @mock.patch('koji_cli._running_in_bg', return_value=False)

-     @mock.patch('koji_cli.watch_tasks', return_value=0)

+     @mock.patch('koji_cli.commands._running_in_bg', return_value=False)

+     @mock.patch('koji_cli.commands.watch_tasks', return_value=0)

      def test_handle_build_invalid_scm(

              self,

              watch_tasks_mock,
@@ -527,7 +528,7 @@ 

          # args: target badscm

          # expected: failed, scm is invalid

          with self.assertRaises(SystemExit) as cm:

-             cli.handle_maven_build(self.options, self.session, args)

+             handle_maven_build(self.options, self.session, args)

          actual = stderr.getvalue()

          expected = """Usage: %s maven-build [options] target URL

         %s maven-build --ini=CONFIG... [options] target
@@ -537,7 +538,7 @@ 

  """ % (progname, progname, progname)

          self.assertMultiLineEqual(actual, expected)

          # Finally, assert that things were called as we expected.

-         activate_session_mock.assert_called_once_with(self.session)

+         activate_session_mock.assert_called_once_with(self.session, self.options)

          self.session.getBuildTarget.assert_called_once_with(target)

          self.session.getTag.assert_called_once_with(dest_tag_id)

          parse_maven_param_mock.assert_not_called()
@@ -549,11 +550,11 @@ 

          self.assertEqual(cm.exception.code, 2)

  

      @mock.patch('sys.stdout', new_callable=six.StringIO)

-     @mock.patch('koji_cli.activate_session')

+     @mock.patch('koji_cli.commands.activate_session')

      @mock.patch('koji.util.parse_maven_param')

      @mock.patch('koji.util.maven_opts', return_value={})

-     @mock.patch('koji_cli._running_in_bg', return_value=False)

-     @mock.patch('koji_cli.watch_tasks', return_value=0)

+     @mock.patch('koji_cli.commands._running_in_bg', return_value=False)

+     @mock.patch('koji_cli.commands.watch_tasks', return_value=0)

      def test_handle_build_other_params(

              self,

              watch_tasks_mock,
@@ -585,14 +586,14 @@ 

          # Run it and check immediate output

          # args: --debug --skip-tag --background target http://scm

          # expected: success

-         rv = cli.handle_maven_build(self.options, self.session, args)

+         rv = handle_maven_build(self.options, self.session, args)

          actual = stdout.getvalue()

          expected = """Created task: 1

  Task info: weburl/taskinfo?taskID=1

  """

          self.assertMultiLineEqual(actual, expected)

          # Finally, assert that things were called as we expected.

-         activate_session_mock.assert_called_once_with(self.session)

+         activate_session_mock.assert_called_once_with(self.session, self.options)

          self.session.getBuildTarget.assert_called_once_with(target)

          self.session.getTag.assert_called_once_with(dest_tag_id)

          parse_maven_param_mock.assert_not_called()
@@ -602,7 +603,8 @@ 

              source, target, opts, priority=priority)

          self.session.logout.assert_called_once()

          watch_tasks_mock.assert_called_once_with(

-             self.session, [task_id], quiet=build_opts['quiet'])

+             self.session, [task_id], quiet=build_opts['quiet'],

+             poll_interval=self.options.poll_interval)

          self.assertEqual(rv, 0)

  

          stdout.seek(0)
@@ -624,16 +626,16 @@ 

          # Run it and check immediate output

          # args: --debug --skip-tag --background -Mtest -Mtest2=val target http://scm

          # expected: success

-         cli.handle_maven_build(self.options, self.session, args)

+         handle_maven_build(self.options, self.session, args)

          self.assertMultiLineEqual(actual, expected)

          maven_opts_mock.assert_called_once_with(build_opts, scratch=scratch)

          self.session.mavenBuild.assert_called_once_with(

              source, target, opts, priority=priority)

  

      @mock.patch('sys.stdout', new_callable=six.StringIO)

-     @mock.patch('koji_cli.activate_session')

-     @mock.patch('koji_cli._running_in_bg', return_value=False)

-     @mock.patch('koji_cli.watch_tasks', return_value=0)

+     @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)

      def test_handle_maven_build_quiet(

              self,

              watch_tasks_mock,
@@ -658,12 +660,12 @@ 

          # Run it and check immediate output

          # args: --quiet target http://scm

          # expected: success

-         rv = cli.handle_maven_build(self.options, self.session, args)

+         rv = handle_maven_build(self.options, self.session, args)

          actual = stdout.getvalue()

          expected = ''

          self.assertMultiLineEqual(actual, expected)

          # Finally, assert that things were called as we expected.

-         activate_session_mock.assert_called_once_with(self.session)

+         activate_session_mock.assert_called_once_with(self.session, self.options)

          self.session.getBuildTarget.assert_called_once_with(target)

          self.session.getTag.assert_called_once_with(dest_tag_id)

          self.session.mavenBuild.assert_called_once_with(
@@ -671,13 +673,14 @@ 

          running_in_bg_mock.assert_called_once()

          self.session.logout.assert_called()

          watch_tasks_mock.assert_called_once_with(

-             self.session, [task_id], quiet=self.options.quiet)

+             self.session, [task_id], quiet=self.options.quiet,

+             poll_interval=self.options.poll_interval)

          self.assertEqual(rv, 0)

  

      @mock.patch('sys.stdout', new_callable=six.StringIO)

-     @mock.patch('koji_cli.activate_session')

-     @mock.patch('koji_cli._running_in_bg', return_value=True)

-     @mock.patch('koji_cli.watch_tasks', return_value=0)

+     @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)

      def test_handle_maven_build_quiet(

              self,

              watch_tasks_mock,
@@ -701,14 +704,14 @@ 

          # Run it and check immediate output

          # args: target http://scm

          # expected: success

-         rv = cli.handle_maven_build(self.options, self.session, args)

+         rv = handle_maven_build(self.options, self.session, args)

          actual = stdout.getvalue()

          expected = """Created task: 1

  Task info: weburl/taskinfo?taskID=1

  """

          self.assertMultiLineEqual(actual, expected)

          # Finally, assert that things were called as we expected.

-         activate_session_mock.assert_called_once_with(self.session)

+         activate_session_mock.assert_called_once_with(self.session, self.options)

          self.session.getBuildTarget.assert_called_once_with(target)

          self.session.getTag.assert_called_once_with(dest_tag_id)

          self.session.mavenBuild.assert_called_once_with(
@@ -719,11 +722,11 @@ 

          self.assertIsNone(rv)

  

      @mock.patch('sys.stdout', new_callable=six.StringIO)

-     @mock.patch('koji_cli.activate_session')

+     @mock.patch('koji_cli.commands.activate_session')

      @mock.patch('koji.util.parse_maven_param')

      @mock.patch('koji.util.maven_opts', return_value={})

-     @mock.patch('koji_cli._running_in_bg', return_value=False)

-     @mock.patch('koji_cli.watch_tasks', return_value=0)

+     @mock.patch('koji_cli.commands._running_in_bg', return_value=False)

+     @mock.patch('koji_cli.commands.watch_tasks', return_value=0)

      def test_handle_maven_build_nowait(

              self,

              watch_tasks_mock,
@@ -752,14 +755,14 @@ 

          # Run it and check immediate output

          # args: target http://scm

          # expected: success

-         rv = cli.handle_maven_build(self.options, self.session, args)

+         rv = handle_maven_build(self.options, self.session, args)

          actual = stdout.getvalue()

          expected = """Created task: 1

  Task info: weburl/taskinfo?taskID=1

  """

          self.assertMultiLineEqual(actual, expected)

          # Finally, assert that things were called as we expected.

-         activate_session_mock.assert_called_once_with(self.session)

+         activate_session_mock.assert_called_once_with(self.session, self.options)

          self.session.getBuildTarget.assert_called_once_with(target)

          self.session.getTag.assert_called_once_with(dest_tag_id)

          parse_maven_param_mock.assert_not_called()

@@ -1,14 +1,11 @@ 

  from __future__ import absolute_import

- import unittest

- 

- import os

- import sys

  import mock

+ import os

  import six

+ import sys

+ import unittest

  

- from . import loadcli

- 

- cli = loadcli.cli

+ from koji_cli.commands import handle_remove_channel

  

  

  class TestRemoveChannel(unittest.TestCase):
@@ -17,7 +14,7 @@ 

      maxDiff = None

  

      @mock.patch('sys.stdout', new_callable=six.StringIO)

-     @mock.patch('koji_cli.activate_session')

+     @mock.patch('koji_cli.commands.activate_session')

      def test_handle_remove_channel(self, activate_session_mock, stdout):

          channel = 'channel'

          channel_info = mock.ANY
@@ -31,18 +28,18 @@ 

          # Run it and check immediate output

          # args: channel

          # expected: success

-         rv = cli.handle_remove_channel(options, session, args)

+         rv = handle_remove_channel(options, session, args)

          actual = stdout.getvalue()

          expected = ''

          self.assertMultiLineEqual(actual, expected)

          # Finally, assert that things were called as we expected.

-         activate_session_mock.assert_called_once_with(session)

+         activate_session_mock.assert_called_once_with(session, options)

          session.getChannel.assert_called_once_with(channel)

          session.removeChannel.assert_called_once_with(channel, force=None)

          self.assertNotEqual(rv, 1)

  

      @mock.patch('sys.stdout', new_callable=six.StringIO)

-     @mock.patch('koji_cli.activate_session')

+     @mock.patch('koji_cli.commands.activate_session')

      def test_handle_remove_channel_force(self, activate_session_mock, stdout):

          channel = 'channel'

          channel_info = mock.ANY
@@ -57,18 +54,18 @@ 

          # Run it and check immediate output

          # args: --force, channel

          # expected: success

-         rv = cli.handle_remove_channel(options, session, args)

+         rv = handle_remove_channel(options, session, args)

          actual = stdout.getvalue()

          expected = ''

          self.assertMultiLineEqual(actual, expected)

          # Finally, assert that things were called as we expected.

-         activate_session_mock.assert_called_once_with(session)

+         activate_session_mock.assert_called_once_with(session, options)

          session.getChannel.assert_called_once_with(channel)

          session.removeChannel.assert_called_once_with(channel, force=True)

          self.assertNotEqual(rv, 1)

  

      @mock.patch('sys.stdout', new_callable=six.StringIO)

-     @mock.patch('koji_cli.activate_session')

+     @mock.patch('koji_cli.commands.activate_session')

      def test_handle_remove_channel_no_channel(

              self, activate_session_mock, stdout):

          channel = 'channel'
@@ -83,19 +80,19 @@ 

          # Run it and check immediate output

          # args: channel

          # expected: failed: no such channel

-         rv = cli.handle_remove_channel(options, session, args)

+         rv = handle_remove_channel(options, session, args)

          actual = stdout.getvalue()

          expected = 'No such channel: channel\n'

          self.assertMultiLineEqual(actual, expected)

          # Finally, assert that things were called as we expected.

-         activate_session_mock.assert_called_once_with(session)

+         activate_session_mock.assert_called_once_with(session, options)

          session.getChannel.assert_called_once_with(channel)

          session.removeChannel.assert_not_called()

          self.assertEqual(rv, 1)

  

      @mock.patch('sys.stdout', new_callable=six.StringIO)

      @mock.patch('sys.stderr', new_callable=six.StringIO)

-     @mock.patch('koji_cli.activate_session')

+     @mock.patch('koji_cli.commands.activate_session')

      def test_handle_remove_channel_help(

              self, activate_session_mock, stderr, stdout):

          args = []
@@ -107,7 +104,7 @@ 

  

          # Run it and check immediate output

          with self.assertRaises(SystemExit) as cm:

-             cli.handle_remove_channel(options, session, args)

+             handle_remove_channel(options, session, args)

          actual_stdout = stdout.getvalue()

          actual_stderr = stderr.getvalue()

          expected_stdout = ''

@@ -1,15 +1,11 @@ 

  from __future__ import absolute_import

- import unittest

- 

- import os

- import sys

  import mock

+ import os

  import six

+ import sys

+ import unittest

  

- from . import loadcli

- 

- cli = loadcli.cli

- 

+ from koji_cli.commands import handle_remove_host_from_channel

  

  class TestRemoveHostFromChannel(unittest.TestCase):

  
@@ -17,7 +13,7 @@ 

      maxDiff = None

  

      @mock.patch('sys.stdout', new_callable=six.StringIO)

-     @mock.patch('koji_cli.activate_session')

+     @mock.patch('koji_cli.commands.activate_session')

      def test_handle_remove_host_from_channel(

              self, activate_session_mock, stdout):

          host = 'host'
@@ -35,19 +31,19 @@ 

          # Run it and check immediate output

          # args: host, channel

          # expected: success

-         rv = cli.handle_remove_host_from_channel(options, session, args)

+         rv = handle_remove_host_from_channel(options, session, args)

          actual = stdout.getvalue()

          expected = ''

          self.assertMultiLineEqual(actual, expected)

          # Finally, assert that things were called as we expected.

-         activate_session_mock.assert_called_once_with(session)

+         activate_session_mock.assert_called_once_with(session, options)

          session.getHost.assert_called_once_with(host)

          session.listChannels.assert_called_once_with(host_info['id'])

          session.removeHostFromChannel.assert_called_once_with(host, channel)

          self.assertNotEqual(rv, 1)

  

      @mock.patch('sys.stdout', new_callable=six.StringIO)

-     @mock.patch('koji_cli.activate_session')

+     @mock.patch('koji_cli.commands.activate_session')

      def test_handle_remove_host_from_channel_no_host(

              self, activate_session_mock, stdout):

          host = 'host'
@@ -63,19 +59,19 @@ 

          # Run it and check immediate output

          # args: host, channel

          # expected: failed: no such host

-         rv = cli.handle_remove_host_from_channel(options, session, args)

+         rv = handle_remove_host_from_channel(options, session, args)

          actual = stdout.getvalue()

          expected = 'No such host: host\n'

          self.assertMultiLineEqual(actual, expected)

          # Finally, assert that things were called as we expected.

-         activate_session_mock.assert_called_once_with(session)

+         activate_session_mock.assert_called_once_with(session, options)

          session.getHost.assert_called_once_with(host)

          session.listChannels.assert_not_called()

          session.removeHostFromChannel.assert_not_called()

          self.assertEqual(rv, 1)

  

      @mock.patch('sys.stdout', new_callable=six.StringIO)

-     @mock.patch('koji_cli.activate_session')

+     @mock.patch('koji_cli.commands.activate_session')

      def test_handle_remove_host_from_channel_not_a_member(

              self, activate_session_mock, stdout):

          host = 'host'
@@ -94,12 +90,12 @@ 

          # Run it and check immediate output

          # args: host, channel

          # expected: success: host isn't belong to channel

-         rv = cli.handle_remove_host_from_channel(options, session, args)

+         rv = handle_remove_host_from_channel(options, session, args)

          actual = stdout.getvalue()

          expected = 'Host host is not a member of channel channel\n'

          self.assertMultiLineEqual(actual, expected)

          # Finally, assert that things were called as we expected.

-         activate_session_mock.assert_called_once_with(session)

+         activate_session_mock.assert_called_once_with(session, options)

          session.getHost.assert_called_once_with(host)

          session.listChannels.assert_called_once_with(host_info['id'])

          session.removeHostFromChannel.assert_not_called()
@@ -107,7 +103,7 @@ 

  

      @mock.patch('sys.stdout', new_callable=six.StringIO)

      @mock.patch('sys.stderr', new_callable=six.StringIO)

-     @mock.patch('koji_cli.activate_session')

+     @mock.patch('koji_cli.commands.activate_session')

      def test_handle_remove_host_from_channel_help(

              self, activate_session_mock, stderr, stdout):

          args = []
@@ -121,7 +117,7 @@ 

          # args: _empty_

          # expected: failed, help msg shows

          with self.assertRaises(SystemExit) as cm:

-             cli.handle_remove_host_from_channel(options, session, args)

+             handle_remove_host_from_channel(options, session, args)

          actual_stdout = stdout.getvalue()

          actual_stderr = stderr.getvalue()

          expected_stdout = ''

@@ -1,17 +1,13 @@ 

  from __future__ import absolute_import

- import unittest

- 

- 

- import os

- import sys

  import mock

+ import os

  import six

+ import sys

+ import unittest

  from mock import call

  

- from . import loadcli

- 

- cli = loadcli.cli

  

+ from koji_cli.commands import handle_remove_pkg

  

  class TestRemovePkg(unittest.TestCase):

  
@@ -19,7 +15,7 @@ 

      maxDiff = None

  

      @mock.patch('sys.stdout', new_callable=six.StringIO)

-     @mock.patch('koji_cli.activate_session')

+     @mock.patch('koji_cli.commands.activate_session')

      def test_handle_remove_pkg(self, activate_session_mock, stdout):

          tag = 'tag'

          dsttag = {'name': tag, 'id': 1}
@@ -37,12 +33,12 @@ 

          # Run it and check immediate output

          # args: tag, package

          # expected: success

-         rv = cli.handle_remove_pkg(options, session, args)

+         rv = handle_remove_pkg(options, session, args)

          actual = stdout.getvalue()

          expected = ''

          self.assertMultiLineEqual(actual, expected)

          # Finally, assert that things were called as we expected.

-         activate_session_mock.assert_called_once_with(session)

+         activate_session_mock.assert_called_once_with(session, options)

          session.getTag.assert_called_once_with(tag)

          session.listPackages.assert_called_once_with(

              tagID=dsttag['id'])
@@ -52,7 +48,7 @@ 

          self.assertNotEqual(rv, 1)

  

      @mock.patch('sys.stdout', new_callable=six.StringIO)

-     @mock.patch('koji_cli.activate_session')

+     @mock.patch('koji_cli.commands.activate_session')

      def test_handle_remove_pkg_multi_pkg(self, activate_session_mock, stdout):

          tag = 'tag'

          dsttag = {'name': tag, 'id': 1}
@@ -74,12 +70,12 @@ 

          # Run it and check immediate output

          # args: tag, package1, package2, package3

          # expected: success

-         rv = cli.handle_remove_pkg(options, session, args)

+         rv = handle_remove_pkg(options, session, args)

          actual = stdout.getvalue()

          expected = ''

          self.assertMultiLineEqual(actual, expected)

          # Finally, assert that things were called as we expected.

-         activate_session_mock.assert_called_once_with(session)

+         activate_session_mock.assert_called_once_with(session, options)

          self.assertEqual(

              session.mock_calls, [

                  call.getTag(tag), call.listPackages(
@@ -91,7 +87,7 @@ 

          self.assertNotEqual(rv, 1)

  

      @mock.patch('sys.stdout', new_callable=six.StringIO)

-     @mock.patch('koji_cli.activate_session')

+     @mock.patch('koji_cli.commands.activate_session')

      def test_handle_remove_pkg_force(self, activate_session_mock, stdout):

          tag = 'tag'

          dsttag = {'name': tag, 'id': 1}
@@ -113,12 +109,12 @@ 

          # Run it and check immediate output

          # args: --force, tag, package1, package2, package3

          # expected: success

-         rv = cli.handle_remove_pkg(options, session, args)

+         rv = handle_remove_pkg(options, session, args)

          actual = stdout.getvalue()

          expected = ''

          self.assertMultiLineEqual(actual, expected)

          # Finally, assert that things were called as we expected.

-         activate_session_mock.assert_called_once_with(session)

+         activate_session_mock.assert_called_once_with(session, options)

          self.assertEqual(

              session.mock_calls, [

                  call.getTag(tag), call.listPackages(
@@ -130,7 +126,7 @@ 

          self.assertNotEqual(rv, 1)

  

      @mock.patch('sys.stdout', new_callable=six.StringIO)

-     @mock.patch('koji_cli.activate_session')

+     @mock.patch('koji_cli.commands.activate_session')

      def test_handle_remove_pkg_no_package(self, activate_session_mock, stdout):

          tag = 'tag'

          dsttag = {'name': tag, 'id': 1}
@@ -149,12 +145,12 @@ 

          # Run it and check immediate output

          # args: tag, package1, package2, package3

          # expected: failed: can not find package2 under tag

-         rv = cli.handle_remove_pkg(options, session, args)

+         rv = handle_remove_pkg(options, session, args)

          actual = stdout.getvalue()

          expected = 'Package package2 is not in tag tag\n'

          self.assertMultiLineEqual(actual, expected)

          # Finally, assert that things were called as we expected.

-         activate_session_mock.assert_called_once_with(session)

+         activate_session_mock.assert_called_once_with(session, options)

          session.getTag.assert_called_once_with(tag)

          session.listPackages.assert_called_once_with(

              tagID=dsttag['id'])
@@ -163,7 +159,7 @@ 

          self.assertEqual(rv, 1)

  

      @mock.patch('sys.stdout', new_callable=six.StringIO)

-     @mock.patch('koji_cli.activate_session')

+     @mock.patch('koji_cli.commands.activate_session')

      def test_handle_remove_pkg_tag_no_exists(

              self, activate_session_mock, stdout):

          tag = 'tag'
@@ -179,12 +175,12 @@ 

          # Run it and check immediate output

          # args: tag, package1, package2, package3

          # expected: failed: tag does not exist

-         rv = cli.handle_remove_pkg(options, session, args)

+         rv = handle_remove_pkg(options, session, args)

          actual = stdout.getvalue()

          expected = 'No such tag: tag\n'

          self.assertMultiLineEqual(actual, expected)

          # Finally, assert that things were called as we expected.

-         activate_session_mock.assert_called_once_with(session)

+         activate_session_mock.assert_called_once_with(session, options)

          session.getTag.assert_called_once_with(tag)

          session.listPackages.assert_not_called()

          session.packageListRemove.assert_not_called()
@@ -192,7 +188,7 @@ 

  

      @mock.patch('sys.stdout', new_callable=six.StringIO)

      @mock.patch('sys.stderr', new_callable=six.StringIO)

-     @mock.patch('koji_cli.activate_session')

+     @mock.patch('koji_cli.commands.activate_session')

      def test_handle_remove_pkg_help(

              self, activate_session_mock, stderr, stdout):

          args = []
@@ -205,7 +201,7 @@ 

  

          # Run it and check immediate output

          with self.assertRaises(SystemExit) as cm:

-             cli.handle_remove_pkg(options, session, args)

+             handle_remove_pkg(options, session, args)

          actual_stdout = stdout.getvalue()

          actual_stderr = stderr.getvalue()

          expected_stdout = ''

@@ -1,14 +1,11 @@ 

  from __future__ import absolute_import

- import unittest

- 

- import os

- import sys

  import mock

+ import os

  import six

+ import sys

+ import unittest

  

- from . import loadcli

- 

- cli = loadcli.cli

+ from koji_cli.commands import handle_rename_channel

  

  

  class TestRenameChannel(unittest.TestCase):
@@ -17,7 +14,7 @@ 

      maxDiff = None

  

      @mock.patch('sys.stdout', new_callable=six.StringIO)

-     @mock.patch('koji_cli.activate_session')

+     @mock.patch('koji_cli.commands.activate_session')

      def test_handle_rename_channel(self, activate_session_mock, stdout):

          old_name = 'old_name'

          new_name = 'new_name'
@@ -32,18 +29,18 @@ 

          # Run it and check immediate output

          # args: old_name, new_name

          # expected: success

-         rv = cli.handle_rename_channel(options, session, args)

+         rv = handle_rename_channel(options, session, args)

          actual = stdout.getvalue()

          expected = ''

          self.assertMultiLineEqual(actual, expected)

          # Finally, assert that things were called as we expected.

-         activate_session_mock.assert_called_once_with(session)

+         activate_session_mock.assert_called_once_with(session, options)

          session.getChannel.assert_called_once_with(old_name)

          session.renameChannel.assert_called_once_with(old_name, new_name)

          self.assertNotEqual(rv, 1)

  

      @mock.patch('sys.stdout', new_callable=six.StringIO)

-     @mock.patch('koji_cli.activate_session')

+     @mock.patch('koji_cli.commands.activate_session')

      def test_handle_rename_channel_no_channel(

              self, activate_session_mock, stdout):

          old_name = 'old_name'
@@ -59,19 +56,19 @@ 

          # Run it and check immediate output

          # args: old_name, new_name

          # expected: failed: no such channel

-         rv = cli.handle_rename_channel(options, session, args)

+         rv = handle_rename_channel(options, session, args)

          actual = stdout.getvalue()

          expected = 'No such channel: old_name\n'

          self.assertMultiLineEqual(actual, expected)

          # Finally, assert that things were called as we expected.

-         activate_session_mock.assert_called_once_with(session)

+         activate_session_mock.assert_called_once_with(session, options)

          session.getChannel.assert_called_once_with(old_name)

          session.renameChannel.assert_not_called()

          self.assertEqual(rv, 1)

  

      @mock.patch('sys.stdout', new_callable=six.StringIO)

      @mock.patch('sys.stderr', new_callable=six.StringIO)

-     @mock.patch('koji_cli.activate_session')

+     @mock.patch('koji_cli.commands.activate_session')

      def test_handle_rename_channel_help(

              self, activate_session_mock, stderr, stdout):

          args = []
@@ -83,7 +80,7 @@ 

  

          # Run it and check immediate output

          with self.assertRaises(SystemExit) as cm:

-             cli.handle_rename_channel(options, session, args)

+             handle_rename_channel(options, session, args)

          actual_stdout = stdout.getvalue()

          actual_stderr = stderr.getvalue()

          expected_stdout = ''

@@ -3,41 +3,38 @@ 

  

  import mock

  

- from . import loadcli

- 

- cli = loadcli.cli

- 

+ from koji_cli.lib import _running_in_bg

  

  class TestRunningInBg(unittest.TestCase):

  

-     @mock.patch('koji_cli.os')

+     @mock.patch('koji_cli.lib.os')

      def test_running_in_bg(self, os_mock):

          os_mock.isatty.return_value = False

-         self.assertTrue(cli._running_in_bg())

+         self.assertTrue(_running_in_bg())

          os_mock.isatty.return_value = True

          os_mock.getpgrp.return_value = 0

          os_mock.tcgetpgrp.return_value = 1

-         self.assertTrue(cli._running_in_bg())

+         self.assertTrue(_running_in_bg())

          os_mock.tcgetpgrp.return_value = 0

-         self.assertFalse(cli._running_in_bg())

+         self.assertFalse(_running_in_bg())

  

          os_mock.reset_mock()

          os_mock.tcgetpgrp.side_effect = OSError

-         self.assertTrue(cli._running_in_bg())

+         self.assertTrue(_running_in_bg())

          os_mock.isatty.assert_called()

          os_mock.getpgrp.assert_called()

          os_mock.tcgetpgrp.assert_called()

  

          os_mock.reset_mock()

          os_mock.getpgrp.side_effect = OSError

-         self.assertTrue(cli._running_in_bg())

+         self.assertTrue(_running_in_bg())

          os_mock.isatty.assert_called()

          os_mock.getpgrp.assert_called()

          os_mock.tcgetpgrp.assert_not_called()

  

          os_mock.reset_mock()

          os_mock.isatty.side_effect = OSError

-         self.assertTrue(cli._running_in_bg())

+         self.assertTrue(_running_in_bg())

          os_mock.isatty.assert_called()

          os_mock.getpgrp.assert_not_called()

          os_mock.tcgetpgrp.assert_not_called()

@@ -1,21 +1,18 @@ 

  from __future__ import absolute_import

  import unittest

- 

- from . import loadcli

  from six.moves import range

  

- cli = loadcli.cli

- 

+ from koji_cli.lib import _unique_path

  

  class TestUniquePath(unittest.TestCase):

  

      def test_unique_path(self):

          for i in range(1000):

              self.assertNotEqual(

-                 cli._unique_path('prefix'),

-                 cli._unique_path('prefix'))

+                 _unique_path('prefix'),

+                 _unique_path('prefix'))

              self.assertRegexpMatches(

-                 cli._unique_path('prefix'),

+                 _unique_path('prefix'),

                  '^prefix/\d{10}\.\d{1,7}\.[a-zA-Z]{8}$')

  

  if __name__ == '__main__':

@@ -1,41 +1,37 @@ 

  from __future__ import absolute_import

- import unittest

  import mock

- import sys

  import six

+ import unittest

  

- from . import loadcli

- 

- cli = loadcli.cli

- 

+ from koji_cli.lib import _format_size, _format_secs, _progress_callback

  

  class TestUploadProgressCallBack(unittest.TestCase):

  

      maxDiff = None

  

      def test_format_size(self):

-         self.assertEqual(cli._format_size(2000000000), '1.86 GiB')

-         self.assertEqual(cli._format_size(1073741824), '1.00 GiB')

-         self.assertEqual(cli._format_size(3000000), '2.86 MiB')

-         self.assertEqual(cli._format_size(1048576), '1.00 MiB')

-         self.assertEqual(cli._format_size(4000), '3.91 KiB')

-         self.assertEqual(cli._format_size(1024), '1.00 KiB')

-         self.assertEqual(cli._format_size(500), '500.00 B')

+         self.assertEqual(_format_size(2000000000), '1.86 GiB')

+         self.assertEqual(_format_size(1073741824), '1.00 GiB')

+         self.assertEqual(_format_size(3000000), '2.86 MiB')

+         self.assertEqual(_format_size(1048576), '1.00 MiB')

+         self.assertEqual(_format_size(4000), '3.91 KiB')

+         self.assertEqual(_format_size(1024), '1.00 KiB')

+         self.assertEqual(_format_size(500), '500.00 B')

  

      def test_format_secs(self):

-         self.assertEqual(cli._format_secs(0), '00:00:00')

-         self.assertEqual(cli._format_secs(60), '00:01:00')

-         self.assertEqual(cli._format_secs(3600), '01:00:00')

-         self.assertEqual(cli._format_secs(7283294), '2023:08:14')

-         self.assertEqual(cli._format_secs(1234), '00:20:34')

-         self.assertEqual(cli._format_secs(4321), '01:12:01')

-         self.assertEqual(cli._format_secs(4321.567), '01:12:01')

+         self.assertEqual(_format_secs(0), '00:00:00')

+         self.assertEqual(_format_secs(60), '00:01:00')

+         self.assertEqual(_format_secs(3600), '01:00:00')

+         self.assertEqual(_format_secs(7283294), '2023:08:14')

+         self.assertEqual(_format_secs(1234), '00:20:34')

+         self.assertEqual(_format_secs(4321), '01:12:01')

+         self.assertEqual(_format_secs(4321.567), '01:12:01')

  

      @mock.patch('sys.stdout', new_callable=six.StringIO)

      def test_progress_callback(self, stdout):

-         cli._progress_callback(12300, 234000, 5670, 80, 900)

-         cli._progress_callback(45600, 234000, 5670, 0, 900)

-         cli._progress_callback(234000, 234000, 5670, 80, 900)

+         _progress_callback(12300, 234000, 5670, 80, 900)

+         _progress_callback(45600, 234000, 5670, 0, 900)

+         _progress_callback(234000, 234000, 5670, 80, 900)

          self.assertMultiLineEqual(

              stdout.getvalue(),

              '[=                                   ]  05% 00:15:00  12.01 KiB    70.88 B/sec\r'

@@ -1,49 +1,38 @@ 

  from __future__ import absolute_import

- import unittest

- 

+ import mock

  import os

+ import six

  import sys

- 

- import mock

+ import unittest

  

  from mock import call

- 

- from . import loadcli

  from six.moves import range

- import six

  

- cli = loadcli.cli

+ from koji_cli.lib import watch_tasks

  

  

  class TestWatchTasks(unittest.TestCase):

  

      def setUp(self):

          self.options = mock.MagicMock()

-         cli.options = self.options

          self.session = mock.MagicMock(name='sessionMock')

          self.args = mock.MagicMock()

-         self.original_parser = cli.OptionParser

-         cli.OptionParser = mock.MagicMock()

-         self.parser = cli.OptionParser.return_value

- 

-     def tearDown(self):

-         cli.OptionParser = self.original_parser

  

      # Show long diffs in error output...

      maxDiff = None

  

      @mock.patch('sys.stdout', new_callable=six.StringIO)

      def test_watch_tasks_no_tasklist(self, stdout):

-         returned = cli.watch_tasks(self.session, [])

+         returned = watch_tasks(self.session, [], poll_interval=0)

          actual = stdout.getvalue()

          expected = ""

          self.assertIsNone(returned)

          self.assertEqual(actual, expected)

  

      @mock.patch('sys.stdout', new_callable=six.StringIO)

-     @mock.patch('koji_cli.TaskWatcher')

-     @mock.patch('koji_cli.display_tasklist_status')

-     @mock.patch('koji_cli.display_task_results')

+     @mock.patch('koji_cli.lib.TaskWatcher')

+     @mock.patch('koji_cli.lib.display_tasklist_status')

+     @mock.patch('koji_cli.lib.display_task_results')

      def test_watch_tasks(self, dtrMock, dtsMock, twClzMock, stdout):

          self.options.poll_interval = 0

          manager = mock.MagicMock()
@@ -78,7 +67,7 @@ 

              return rt

  

          twClzMock.side_effect = side_effect

-         rv = cli.watch_tasks(self.session, list(range(2)), quiet=False)

+         rv = watch_tasks(self.session, list(range(2)), quiet=False, poll_interval=0)

          actual = stdout.getvalue()

          self.assertMultiLineEqual(

              actual, "Watching tasks (this may be safely interrupted)...\n\n")
@@ -171,9 +160,9 @@ 

                            ])

  

      @mock.patch('sys.stdout', new_callable=six.StringIO)

-     @mock.patch('koji_cli.TaskWatcher')

-     @mock.patch('koji_cli.display_tasklist_status')

-     @mock.patch('koji_cli.display_task_results')

+     @mock.patch('koji_cli.lib.TaskWatcher')

+     @mock.patch('koji_cli.lib.display_tasklist_status')

+     @mock.patch('koji_cli.lib.display_task_results')

      def test_watch_tasks_with_keyboardinterrupt(

              self, dtrMock, dtsMock, twClzMock, stdout):

          """Raise KeyboardInterrupt inner watch_tasks.
@@ -217,7 +206,7 @@ 

          twClzMock.side_effect = side_effect

  

          with self.assertRaises(KeyboardInterrupt):

-             cli.watch_tasks(self.session, list(range(2)), quiet=False)

+             watch_tasks(self.session, list(range(2)), quiet=False, poll_interval=0)

  

          actual = stdout.getvalue()

          self.assertMultiLineEqual(

@@ -0,0 +1,28 @@ 

+ import os

+ import sys

+ 

+ def load_plugin(plugin_type, plugin_name):

+     # We have to do this craziness because 'import koji' is ambiguous.  Is it the

+     # koji module, or the koji cli module.  Jump through hoops accordingly.

+     # https://stackoverflow.com/questions/67631/how-to-import-a-module-given-the-full-path

+     mod_name = "%s_%s" % (plugin_name, plugin_type)

+     CLI_FILENAME = os.path.join(

+         os.path.dirname(__file__),

+         "../../plugins",

+         plugin_type,

+         "%s.py" % plugin_name)

+     sys.path = [os.path.dirname(CLI_FILENAME),

+                 os.path.join(os.path.dirname(__file__), "../..", plugin_type)] + \

+                sys.path

+     if sys.version_info[0] >= 3:

+         import importlib.machinery

+         loader = importlib.machinery.SourceFileLoader(mod_name, CLI_FILENAME)

+         spec = importlib.util.spec_from_loader(loader.name, loader)

+         kojid = importlib.util.module_from_spec(spec)

+         spec.loader.exec_module(kojid)

+         loader.exec_module(kojid)

+         sys.modules[mod_name] = kojid

+     else:

+         import imp

+         plugin = imp.load_source(mod_name, CLI_FILENAME)

+     return plugin

tests/test_plugins/test_runroot_cli.py tests/test_cli/test_runroot.py
file renamed
+8 -16
@@ -1,15 +1,12 @@ 

  from __future__ import absolute_import

- import os

- import sys

- import unittest

- import koji

- import six

- 

  import mock

+ import six

+ import unittest

  

- from . import loadcli

- cli = loadcli.cli

+ import koji

+ from . import load_plugin

  

+ runroot = load_plugin.load_plugin('cli', 'runroot')

  

  class TestListCommands(unittest.TestCase):

  
@@ -18,13 +15,8 @@ 

          self.session = mock.MagicMock()

          self.session.getAPIVersion.return_value = koji.API_VERSION

          self.args = mock.MagicMock()

-         self.original_parser = cli.OptionParser

-         cli.OptionParser = mock.MagicMock()

-         self.parser = cli.OptionParser.return_value

-         cli.options = self.options  # globals!!!

- 

-     def tearDown(self):

-         cli.OptionParser = self.original_parser

+         runroot.OptionParser = mock.MagicMock()

+         self.parser = runroot.OptionParser.return_value

  

      # Show long diffs in error output...

      maxDiff = None
@@ -46,7 +38,7 @@ 

          self.session.runroot.return_value = 1

  

          # Run it and check immediate output

-         cli.handle_runroot(self.options, self.session, self.args)

+         runroot.handle_runroot(self.options, self.session, self.args)

          actual = stdout.getvalue()

          actual = actual.replace('nosetests', 'koji')

          expected = 'successfully connected to hub\n1\ntask output'

tests/test_plugins/test_save_failed_tree_cli.py tests/test_cli/test_save_failed_tree.py
file renamed
+33 -42
@@ -1,31 +1,27 @@ 

- from __future__ import absolute_import

+ import mock

+ import StringIO

  import unittest

+ 

  import koji

- import mock

- import six

  

- from . import loadcli

- cli = loadcli.cli

+ import load_plugin

+ save_failed_tree = load_plugin.load_plugin('cli', 'save_failed_tree')

  

  

  class TestSaveFailedTree(unittest.TestCase):

      def setUp(self):

-         self.options = mock.MagicMock()

          self.session = mock.MagicMock()

          self.args = mock.MagicMock()

-         self.original_parser = cli.OptionParser

-         cli.OptionParser = mock.MagicMock()

-         self.parser = cli.OptionParser.return_value

-         cli.options = self.options  # globals!!!

- 

-     def tearDown(self):

-         cli.OptionParser = self.original_parser

+         self.parser = mock.MagicMock()

+         save_failed_tree.activate_session = mock.MagicMock()

+         save_failed_tree.OptionParser = mock.MagicMock()

+         save_failed_tree.watch_tasks = mock.MagicMock()

+         self.parser = save_failed_tree.OptionParser.return_value

  

      # Show long diffs in error output...

      maxDiff = None

  

-     @mock.patch('koji_cli.activate_session')

-     def test_handle_save_failed_tree_simple(self, activate_session_mock):

+     def test_handle_save_failed_tree_simple(self ):

          # koji save-failed-tree 123456

          task_id = 123456

          broot_id = 321
@@ -41,15 +37,14 @@ 

          self.session.saveFailedTree.return_value = 123

  

          # Run it and check immediate output

-         cli.handle_save_failed_tree(self.options, self.session, self.args)

+         save_failed_tree.handle_save_failed_tree(options, self.session, self.args)

  

          # Finally, assert that things were called as we expected.

-         activate_session_mock.assert_called_once_with(self.session)

+         save_failed_tree.activate_session.assert_called_once_with(self.session, options)

          self.session.listBuildroots.assert_called_once_with(taskID=task_id)

          self.session.saveFailedTree.assert_called_once_with(broot_id, options.full)

  

-     @mock.patch('koji_cli.activate_session')

-     def test_handle_save_failed_tree_buildroots(self, activate_session_mock):

+     def test_handle_save_failed_tree_buildroots(self):

          # koji save-failed-tree --buildroot 123456

          broot_id = 321

          arguments = [broot_id]
@@ -65,16 +60,15 @@ 

          self.session.saveFailedTree.return_value = 123

  

          # Run it and check immediate output

-         cli.handle_save_failed_tree(self.options, self.session, self.args)

+         save_failed_tree.handle_save_failed_tree(options, self.session, self.args)

  

          # Finally, assert that things were called as we expected.

-         activate_session_mock.assert_called_once_with(self.session)

+         save_failed_tree.activate_session.assert_called_once_with(self.session, options)

          self.session.listBuildroots.assert_not_called()

          self.session.saveFailedTree.assert_called_once_with(broot_id, options.full)

  

  

-     @mock.patch('koji_cli.activate_session')

-     def test_handle_save_failed_tree_full(self, activate_session_mock):

+     def test_handle_save_failed_tree_full(self):

          # koji save-failed-tree 123456 --full

          task_id = 123456

          broot_id = 321
@@ -90,16 +84,14 @@ 

          self.session.saveFailedTree.return_value = 123

  

          # Run it and check immediate output

-         cli.handle_save_failed_tree(self.options, self.session, self.args)

+         save_failed_tree.handle_save_failed_tree(options, self.session, self.args)

  

          # Finally, assert that things were called as we expected.

-         activate_session_mock.assert_called_once_with(self.session)

+         save_failed_tree.activate_session.assert_called_once_with(self.session, options)

          self.session.listBuildroots.assert_called_once_with(taskID=task_id)

          self.session.saveFailedTree.assert_called_once_with(broot_id, options.full)

  

-     @mock.patch('koji_cli.activate_session')

-     @mock.patch('koji_cli.watch_tasks')

-     def test_handle_save_failed_tree_wait(self, watch_tasks_mock, activate_session_mock):

+     def test_handle_save_failed_tree_wait(self):

          # koji save-failed-tree 123456 --full

          task_id = 123456

          broot_id = 321
@@ -117,20 +109,19 @@ 

          self.session.saveFailedTree.return_value = spawned_id

  

          # Run it and check immediate output

-         cli.handle_save_failed_tree(self.options, self.session, self.args)

+         save_failed_tree.handle_save_failed_tree(options, self.session, self.args)

  

          # Finally, assert that things were called as we expected.

          self.session.listBuildroots.assert_called_once_with(taskID=task_id)

          self.session.saveFailedTree.assert_called_once_with(broot_id, options.full)

-         activate_session_mock.assert_called_once_with(self.session)

+         save_failed_tree.activate_session.assert_called_once_with(self.session, options)

          self.session.logout.assert_called_once_with()

-         watch_tasks_mock.assert_called_once_with(self.session, [spawned_id],

-                                                  quiet=options.quiet)

+         save_failed_tree.watch_tasks.assert_called_once_with(self.session, [spawned_id],

+                                                              poll_interval=options.poll_interval,

+                                                              quiet=options.quiet)

  

-     @mock.patch('sys.stdout', new_callable=six.StringIO)

-     @mock.patch('koji_cli.activate_session')

-     @mock.patch('koji_cli.watch_tasks')

-     def test_handle_save_failed_tree_errors(self, watch_tasks_mock, activate_session_mock, stdout):

+     @mock.patch('sys.stdout', new_callable=StringIO.StringIO)

+     def test_handle_save_failed_tree_errors(self, stdout):

          # koji save-failed-tree 123 456

          arguments = [123, 456]

          options = mock.MagicMock()
@@ -139,26 +130,26 @@ 

          self.session.getAPIVersion.return_value = koji.API_VERSION

          self.session.listBuildroots.return_value = [{'id': 321}]

  

-         self.assertRaises(Exception, cli.handle_save_failed_tree,

-                           self.options, self.session, self.args)

+         self.assertRaises(Exception, save_failed_tree.handle_save_failed_tree,

+                           options, self.session, self.args)

  

          arguments = ["text"]

          self.parser.parse_args.return_value = [options, arguments]

-         self.assertRaises(Exception, cli.handle_save_failed_tree, self.options,

+         self.assertRaises(Exception, save_failed_tree.handle_save_failed_tree, options,

                            self.session, self.args)

-         cli.logger = mock.MagicMock()

+         save_failed_tree.logger = mock.MagicMock()

  

          # plugin not installed

          arguments = [123]

          self.parser.parse_args.return_value = [options, arguments]

          self.session.saveFailedTree.side_effect = koji.GenericError("Invalid method")

-         cli.handle_save_failed_tree(self.options, self.session, self.args)

+         save_failed_tree.handle_save_failed_tree(options, self.session, self.args)

          actual = stdout.getvalue()

          self.assertTrue('The save_failed_tree plugin appears to not be installed' in actual)

  

          # Task which is not FAILED, disabled in config, wrong owner

          self.session.saveFailedTree.side_effect = koji.PreBuildError('placeholder')

          with self.assertRaises(koji.PreBuildError) as cm:

-             cli.handle_save_failed_tree(self.options, self.session, self.args)

+             save_failed_tree.handle_save_failed_tree(options, self.session, self.args)

          e = cm.exception

          self.assertEqual(e, self.session.saveFailedTree.side_effect)

Removed runroot plugin CLI part to separate file (plugins/cli/runroot.py) and altered behaviour of CLI to look in pluginspath and load all found commands. Commands are injected to CLI namespace which is not intuitive and maybe thing to change.

My reason to do that was, that CLI is heavily influencing its behaviour in main function. If we don't want to go this way, other option is to get rid of CLI globals and make plugins more independent on CLI code.

Suggestions?

Note: What is missing are .spec modifications - as it could be influenced by suggested changes.

the test_runroot.py test is failing

Yeah, the global injection seems a little heavy handed. Surely there is a better way. Granted that may mean moving more code around.

3 new commits added

  • Updated build scripts
  • tests
  • split
7 years ago

I did so - koji_cli module created. koji_cli.lib contains all reused functions, while koji_cli.commands contains all default commands. Import of commands is now via 'normal' import and plugins will behave almost exactly as standard commands.
Also tests were little bit simplified as there is no need to do magic of loading koji 'binary'.

Why are plugins being installed to /usr/lib/koji-cli-plugins? If they're executable scripts called upon by Koji, shouldn't they default to /usr/libexec/koji/cli-plugins or something like that? Alternatively, if they could be treated as functionality equivalent to regular python imports, then they could be in %{python_sitelib}/koji-cli-plugins instead?

My reasoning was to be coherent with how koji-hub-plugins and koji-builder-plugins are handled. But yes, you're right and pyhon_sitelib would be more appropriate place.
@mikem is there any reason to stay with these 'old' paths? Or could we move all these three to sitelib?

@tkopecek I would support moving them to sitelib and treating them like regular imports. Let's not be special if we don't have to be.

4 new commits added

  • Updated plugins location and build scripts
  • tests
  • split
  • CLI plugins
7 years ago

Plugins moved to {sitelib}/koji_cli_plugins, build scripts updated.

rebased

7 years ago

rebased

7 years ago

5 new commits added

  • fix maven tests
  • Updated plugins location and build scripts
  • tests
  • split
  • CLI plugins
7 years ago

rebased to master and fixed test which appeared meanwhile

rebased

7 years ago

Should I rebase it to current master, or are there some more design comments?

rebased

7 years ago

rebased

7 years ago

I would also like to make commands a module instead of one (still big) file. But I've never seen rationale for having all of it in one file (same for kojid). So, is there any, or could we go kobo way - one file per command in commands module?

I would also like to make commands a module instead of one (still big) file. But I've never seen rationale for having all of it in one file (same for kojid). So, is there any, or could we go kobo way - one file per command in commands module?

I don't think there's an intrinsic reason or anything.

If we go to separate files, then seems like we'd only want to import them on demand. I suspect if we're going to import all of it, it's probably faster as one file.

If we do import them on demand, then we have to sort out just how that will work. The help command will probably need to load them all, but the rest should only need to load themselves.

Loading just the command we need means we need to know in advance where to load it from, which suggests that the file/module name should be derived from the command name.

Need to fix the Makefile changes to use tabs, make is complaining

...except the ifneq/endif lines, those need to not be indented I think (otherwise make install fails)

default value of plugins means that command fails at start if koji-cli-plugins is not installed.

Having pluginpath and plugins in read_config(), which lives in the main lib and is used by other tools, seems questionable. This isn't just for the cli anymore.

This is a tricky issue. I'm not sure what the best answer is here.

2 new commits added

  • skip non-existing plugin path
  • tabs instead of spaces in Makefiles
6 years ago

To pluginpath: Other options could be to have separate koji cli config, section in koji.conf [cli] or leave it as it is. Also not sure.

rebased

6 years ago

rebased

6 years ago

10 new commits added

  • spec update
  • Makefile updates
  • update cli tests
  • refactor activate_session
  • fix entry path
  • cleanup after automatic split
  • apply split
  • touch init.py in koji_cli
  • update split_cli.py for builtins
  • script for split
6 years ago

rebased

6 years ago
  • split_cli script used for initial split instead of manual work
  • updated to python3 structure
  • activate_session has easier changes (options -> goptions if needed)
  • all tests passing

rebased

6 years ago

@mikem - one more thought about configuration. Option could be to don't make it configurable. It would differ from hub/builder, but on the other side, every plugin should fail correctly if it is not supported on hub. So, everything found in site-packages/koji_cli_plugins would be loaded and no other (or predefined path in home directory) and nothing more. Would it be better?

rebased

6 years ago

3 new commits added

  • Fix blank lines (pep8)
  • CLI plugin dev docs
  • remove CLI plugins config options
6 years ago

Installed package fails to run because the python3 lib doesn't include koji.plugin. Fix here

https://github.com/mikem23/koji-playground/commits/issue193

Afew more fixes on the branch linked above. Some are more general py3 fixes that I ran across while testing.

6 new commits added

  • strip .py from module name
  • watch-logs: fix py3 encoding issues
  • fix command options that default to global option value
  • avoid sorting dictionary fields
  • cli makefile: make sure we create dir before installing to it
  • include plugin.py in py3 lib
6 years ago

Added your commits + typo fix in rewritten plugin search.

Commit 9cfadf7 fixes this pull-request

Pull-Request has been merged by mikem@redhat.com

6 years ago
Changes Summary 43
+2 -2
file changed
Makefile
+13 -0
file changed
cli/Makefile
+122 -7501
file changed
cli/koji
+3 -0
file changed
cli/koji.conf
+28
file added
cli/koji_cli/Makefile
+0
file added
cli/koji_cli/__init__.py
+6931
file added
cli/koji_cli/commands.py
+525
file added
cli/koji_cli/lib.py
+60 -0
file changed
docs/source/writing_a_plugin.rst
+42 -0
file changed
koji.spec
+1 -1
file changed
koji/Makefile
+2 -1
file changed
koji/__init__.py
+9 -0
file changed
koji/plugin.py
+28 -10
file changed
plugins/Makefile
+84
file added
plugins/cli/runroot.py
+67
file added
plugins/cli/save_failed_tree.py
+104
file added
split_cli.py
+0 -1
file changed
tests/test_cli/data/list-commands-admin.txt
+0 -2
file changed
tests/test_cli/data/list-commands.txt
+18 -22
file changed
tests/test_cli/test_add_group.py
+18 -22
file changed
tests/test_cli/test_add_host.py
+21 -24
file changed
tests/test_cli/test_add_host_to_channel.py
+19 -21
file changed
tests/test_cli/test_add_pkg.py
+18 -21
file changed
tests/test_cli/test_block_pkg.py
+117 -112
file changed
tests/test_cli/test_build.py
+68 -67
file changed
tests/test_cli/test_chain_build.py
+18 -22
file changed
tests/test_cli/test_edit_host.py
+13 -16
file changed
tests/test_cli/test_edit_tag.py
+48 -48
file changed
tests/test_cli/test_import_comps.py
+9 -18
file changed
tests/test_cli/test_list_channels.py
+1 -1
file changed
tests/test_cli/test_list_commands.py
+79 -76
file changed
tests/test_cli/test_maven_build.py
+15 -18
file changed
tests/test_cli/test_remove_channel.py
+15 -19
file changed
tests/test_cli/test_remove_host_from_channel.py
+21 -25
file changed
tests/test_cli/test_remove_pkg.py
+12 -15
file changed
tests/test_cli/test_rename_channel.py
+8 -11
file changed
tests/test_cli/test_running_in_bg.py
+4 -7
file changed
tests/test_cli/test_unique_path.py
+19 -23
file changed
tests/test_cli/test_upload_progress_callback.py
+13 -24
file changed
tests/test_cli/test_watch_tasks.py
+28
file added
tests/test_plugins/load_plugin.py
+8 -16
file renamed
tests/test_cli/test_runroot.py
tests/test_plugins/test_runroot_cli.py
+33 -42
file renamed
tests/test_cli/test_save_failed_tree.py
tests/test_plugins/test_save_failed_tree_cli.py