#!/usr/bin/python

# command line interface for the Koji build system
# Copyright (c) 2005-2007 Red Hat
#
#    Koji is free software; you can redistribute it and/or
#    modify it under the terms of the GNU Lesser General Public
#    License as published by the Free Software Foundation; 
#    version 2.1 of the License.
#
#    This software is distributed in the hope that it will be useful,
#    but WITHOUT ANY WARRANTY; without even the implied warranty of
#    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
#    Lesser General Public License for more details.
#
#    You should have received a copy of the GNU Lesser General Public
#    License along with this software; if not, write to the Free Software
#    Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
#
# Authors:
#       Dennis Gregorovic <dgregor@redhat.com>
#       Mike McLean <mikem@redhat.com>
import sys
try:
    import krbV
except ImportError:
    pass
import ConfigParser
import base64
import koji
import fnmatch
import md5
import os
import re
import pprint
import random
import socket
import string
import time
import urllib
import urlgrabber.grabber as grabber
import urlgrabber.progress as progress
import xmlrpclib
import optparse
#for import-comps handler (currently disabled)
#from rhpl.comps import Comps

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

def _(args):
    """Stub function for translation"""
    return args

def arg_filter(str):
    try:
        return int(str)
    except ValueError:
        pass
    try:
        return float(str)
    except ValueError:
        pass
    #handle lists?
    return str

def get_options():
    """process options from command line and config file"""

    usage = _("%prog [global-options] command [command-options-and-arguments]")
    parser = OptionParser(usage=usage)
    parser.disable_interspersed_args()
    parser.add_option("-c", "--config", dest="configFile",
                      help=_("use alternate configuration file"), metavar="FILE",
                      default="~/.koji/config")
    parser.add_option("--keytab", help=_("specify a Kerberos keytab to use"), metavar="FILE")
    parser.add_option("--principal", help=_("specify a Kerberos principal to use"))
    parser.add_option("--runas", help=_("run as the specified user (requires special privileges)"))
    parser.add_option("--user", help=_("specify user"))
    parser.add_option("--password", help=_("specify password"))
    parser.add_option("--noauth", action="store_true", default=False,
                      help=_("do not authenticate"))
    parser.add_option("--force-auth", action="store_true", default=False,
                      help=_("authenticate even for read-only operations"))
    parser.add_option("-d", "--debug", action="store_true", default=False,
                      help=_("show debug output"))
    parser.add_option("--debug-xmlrpc", action="store_true", default=False,
                      help=_("show xmlrpc debug output"))
    parser.add_option("--skip-main", action="store_true", default=False,
                      help=_("don't actually run main"))
    parser.add_option("-s", "--server", help=_("url of XMLRPC server"))
    parser.add_option("--topdir", help=_("specify topdir"))
    parser.add_option("--weburl", help=_("url of the Koji web interface"))
    parser.add_option("--pkgurl", help=_("url of the Koji package tree"))
    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',
    }
    cmd = args[0]
    cmd = aliases.get(cmd, cmd)
    cmd = cmd.replace('-', '_')
    if globals().has_key('anon_handle_' + cmd):
        if not options.force_auth:
            options.noauth = True
        cmd = 'anon_handle_' + cmd
    elif globals().has_key('handle_' + cmd):
        cmd = 'handle_' + cmd
    else:
        list_commands()
        parser.error('Unknown command: %s' % cmd)
        assert False
    # load local config
    defaults = {
        'server' : 'http://localhost/kojihub',
        'weburl' : 'http://localhost/koji',
        'pkgurl' : 'http://localhost/packages',
        'topdir' : '/mnt/koji',
        'cert': '~/.koji/client.crt',
        'ca': '~/.koji/clientca.crt',
        'serverca': '~/.koji/serverca.crt'
        }
    # grab settings from /etc/koji.conf first, and allow them to be
    # overridden by user config
    for configFile in ('/etc/koji.conf', os.path.expanduser(options.configFile)):
        if os.access(configFile, os.F_OK):
            f = open(configFile)
            config = ConfigParser.ConfigParser()
            config.readfp(f)
            f.close()
            if config.has_section('koji'):
                for name, value in config.items('koji'):
                    #note the defaults dictionary also serves to indicate which
                    #options *can* be set via the config file. Such options should
                    #not have a default value set in the option parser.
                    if defaults.has_key(name):
                        defaults[name] = value
    for name, value in defaults.iteritems():
        if getattr(options, name, None) is None:
            setattr(options, name, value)
    dir_opts = ('topdir', 'cert', 'ca', 'serverca')
    for name in dir_opts:
        # expand paths here, so we don't have to worry about it later
        value = os.path.expanduser(getattr(options, name))
        setattr(options, name, value)
    
    return options, cmd, args[1:]

def ensure_connection(session):
    try:
        ret = session.getAPIVersion()
    except xmlrpclib.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')
    fmt1 = "%(id)-5s %(priority)-4s %(owner)-12s %(state)-8s %(arch)-10s "
    fmt2 = "%(method)s"
    if depth:
        indent = "  "*(depth-1) + " +"
    else:
        indent = ''
    if task.get('host'):
        fmt3 = ' [%(host)s]'
    else:
        fmt3 = ''
    if task.get('build_id'):
        fmt4 = ' %(build_name)s-%(build_version)s-%(build_release)s'
    else:
        fmt4 = ''
    print ''.join([fmt1 % task, indent, fmt2 % task, fmt3 % task, fmt4 % task])

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)


class TaskWatcher(object):

    def __init__(self,task_id,session,level=0):
        self.id = task_id
        self.session = session
        self.info = None
        self.level = level

    #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 (xmlrpclib.Fault,koji.GenericError),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:
            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:
                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
            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):
        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):
    if not tasklist:
        return
    print "Watching tasks (this may be safely interrupted)..."
    rv = 0
    try:
        tasks = {}
        for task_id in tasklist:
            tasks[task_id] = TaskWatcher(task_id,session)
        while True:
            all_done = True
            for task_id,task in tasks.items():
                changed = task.update()
                if not task.is_done():
                    all_done = False
                elif changed:
                    # task is done and state just changed
                    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 tasks.keys():
                        tasks[child_id] = TaskWatcher(child_id, session, task.level + 1)
                        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:
                print
                display_task_results(tasks)
                break

            time.sleep(1)
    except (KeyboardInterrupt):
        if tasks:
            print \
"""Tasks still running. You can continue to watch with the 'koji watch-task' command.
Running Tasks:
%s""" % '\n'.join(['%s: %s' % (t.str(), t.display_state(t.info)) for t in tasks.values() if not t.is_done()])
        rv = 1
    return rv

def watch_logs(session, tasklist, 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'])

    try:
        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 = session.listTaskOutput(task_id)

                if options.log:
                    logs = [filename for filename in output if filename == options.log]
                else:
                    logs = [filename for filename in output if filename.endswith('.log')]

                taskoffsets = offsets[task_id]
                for log in logs:
                    contents = 'placeholder'
                    while contents:
                        if not taskoffsets.has_key(log):
                            taskoffsets[log] = 0
                        
                        contents = session.downloadTaskOutput(task_id, log, taskoffsets[log], 16384)
                        taskoffsets[log] += len(contents)
                        if contents:
                            currlog = "%d:%s:" % (task_id, 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(1)
    except (KeyboardInterrupt):
        pass

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)
    (options, args) = parser.parse_args(args)
    if len(args) < 2:
        parser.error(_("Please specify a hostname and at least one arch"))
        assert False
    host = args[0]
    activate_session(session)
    id = session.getHost(host)
    if id:
        print "%s is already in the database, skipping" % host
    else:
        id = session.addHost(host, args[1:])
        if id:
            print "%s added: id %d" % (host, id)

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)
    (options, args) = parser.parse_args(args)
    if len(args) != 2:
        parser.error(_("Please specify a hostname and a channel"))
        assert False
    host = args[0]
    activate_session(session)
    id = session.getHost(host)
    if not id:
        print "%s is not a host" % host
        return 1
    session.addHostToChannel(host, args[1])

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
    host = args[0]
    activate_session(session)
    id = session.getHost(host)
    if not id:
        print "%s is not a host" % host
        return 1
    session.removeHostFromChannel(host, args[1])

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
    activate_session(session)
    tag = args[0]
    opts = {}
    opts['force'] = options.force
    opts['block'] = False
    if options.extra_arches:
        opts['extra_arches'] = ' '.join(options.extra_arches.replace(',',' ').split())
    for package in args[1:]:
        #really should implement multicall...
        session.packageListAdd(tag,package,options.owner,**opts)

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 ...]")
    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
    activate_session(session)
    tag = args[0]
    for package in args[1:]:
        #really should implement multicall...
        session.packageListBlock(tag,package)

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 Gb" % (size / 1073741824.0)
    if (size / 1048576 >= 1):
        return "%0.2f Mb" % (size / 1048576.0)
    if (size / 1024 >=1):
        return "%0.2f Kb" % (size / 1024.0)
    return "%0.2f B" % (size)

def _format_secs(t):
    h = t / 3600
    t = t % 3600
    m = t / 60
    s = t % 60
    return "%02d:%02d:%02d" % (h, m, s)

def _progress_callback(uploaded, total, piece, time, total_time):
    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:
        if (not os.isatty(0)) or (os.getpgrp() != os.tcgetpgrp(0)):
            return True
    except OSError, e:
        return True
    return False

def handle_build(options, session, args):
    "Build a package from source"
    usage = _("usage: %prog build [options] target 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("--nowait", action="store_true",
                      help=_("Don't wait on build"))
    parser.add_option("--arch-override", help=_("Override build arches"))
    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 CVS URL or srpm file) are required"))
        assert False
    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]
    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'] = ' '.join(build_opts.arch_override.replace(',',' ').split())
    for key in ('skip_tag','scratch'):
        opts[key] = getattr(build_opts,key)
    priority = None
    if build_opts.background:
        #relative to koji.PRIO_DEFAULT
        priority = 5
    if not source.startswith('cvs://'):
        # only allow admins to perform non-scratch builds from srpm
        if not opts['scratch'] and not session.hasPerm('admin'):
            parser.error(_("builds from srpm must use --scratch"))
            assert False
        #treat source as an srpm and upload it
        print "Uploading srpm: %s" % source
        serverdir = _unique_path('cli-build')
        if _running_in_bg() or build_opts.noprogress:
            callback = None
        else:
            callback = _progress_callback
        session.uploadWrapper(source, serverdir, callback=callback, blocksize=65536)
        print
        source = "%s/%s" % (serverdir, os.path.basename(source))
    task_id = session.build(source, target, opts, priority=priority)
    print "Created task:", task_id
    print "Task info: %s/taskinfo?taskID=%s" % (options.weburl, task_id)
    if _running_in_bg() or build_opts.nowait:
        return
    else:
        return watch_tasks(session,[task_id])

def handle_chain_build(options, session, args):
    # XXX - replace handle_build with this, once chain-building has gotten testing
    "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("--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(_("At least two arguments (a build target and a CVS URL or srpm file) are required"))
        assert False
    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']:
        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 [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:]
    
    srpms = {}
    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 not src.startswith('cvs://'):
            serverpath = "%s/%s" % (_unique_path('cli-build'), os.path.basename(src))
            srpms[src] = serverpath
            build_level.append(serverpath)
        else:
            build_level.append(src)
    if build_level:
        src_list.append(build_level)

    if len(src_list) < 2:
        parser.error(_('you must specify at least one dependency between builds with : (colon)\nif there are no dependencies, use the build command instead'))

    priority = None
    if build_opts.background:
        #relative to koji.PRIO_DEFAULT
        priority = 5
    
    if srpms:
        print "Uploading SRPMs:"
        if _running_in_bg() or build_opts.noprogress:
            callback = None
        else:
            callback = _progress_callback
        for source, dest in srpms.items():
            print os.path.basename(source)
            #uploadWrapper wants the destination dir
            dest = os.path.dirname(dest)
            session.uploadWrapper(source, dest, callback=callback, blocksize=65536)
            print

    task_id = session.chainBuild(src_list, target, priority=priority)

    print "Created task:", task_id
    print "Task info: %s/taskinfo?taskID=%s" % (options.weburl, task_id)
    if _running_in_bg() or build_opts.nowait:
        return
    else:
        return watch_tasks(session,[task_id])

def handle_resubmit(options, session, args):
    """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"))
    (options, args) = parser.parse_args(args)
    if len(args) < 1:
        parser.error(_("Please specify a task ID"))
        assert False
    activate_session(session)
    taskID = args[0]
    newID = session.resubmitTask(int(taskID))
    print "Resubmitted task %s as new task %s" % (taskID, newID)
    if _running_in_bg() or options.nowait:
        return
    else:
        watch_tasks(session,[newID])

def handle_call(options, session, args):
    "[admin] 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)
    (options, args) = parser.parse_args(args)
    if len(args) < 1:
        parser.error(_("Please specify the name of the XML-RPC method"))
        assert False
    activate_session(session)
    name = args[0]
    non_kw = []
    kw = {}

    def _convarg(val):
        valmap = {'None': None,
                  'True': True,
                  'False': False}
        
        if val.isdigit():
            return int(val)
        elif valmap.has_key(val):
            return valmap[val]
        else:
            return val
    
    for arg in args[1:]:
        if arg.find('=') != -1:
            key, value = arg.split('=', 1)
            kw[key] = _convarg(value)
        else:
            non_kw.append(_convarg(arg))
    pprint.pprint(getattr(session, name).__call__(*non_kw, **kw))

def anon_handle_mock_config(options, session, args):
    "Create a mock config"
    usage = _("usage: %prog mock-config [options] name")
    usage += _("\n(Specify the --help global option for a list of other help options)")
    parser = OptionParser(usage=usage)
    parser.add_option("--arch", help=_("Specify the arch"))
    parser.add_option("--tag", help=_("Create a mock config for a tag"))
    parser.add_option("--task", help=_("Duplicate the mock config of a previous task"))
    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",
                      help=_("url under which Koji files are accessible"))
    parser.add_option("--distribution", default="Koji Testing",
                      help=_("Change the distribution macro"))
    parser.add_option("-o", metavar="FILE", dest="ofile", help=_("Output to a file"))
    (options, args) = parser.parse_args(args)
    if len(args) != 1:
        parser.error(_("Please specify and name for your buildroot"))
        assert False
    activate_session(session)
    name = args[0]
    arch = None
    opts = {}
    for k in ('topdir', 'topurl', 'distribution', 'mockdir'):
        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)
        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
            sys.exit(1)
        if len(broots) > 1:
            print _("Multiple buildroots found: %s" % [br['id'] for br in broots])
        brootinfo = broots[0]
        opts['repoid'] = brootinfo['repo_id']
        opts['tag_name'] = brootinfo['tag_name']
        arch = brootinfo['arch']
    elif options.tag:
        if not options.arch:
            print _("Please specify an arch")
            sys.exit(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
            sys.exit(1)
        opts['tag_name'] = tag['name']
        repo = session.getRepo(config['id'])
        if not repo:
            print _("Could not get a repo for tag: %(name)s") % tag
            sys.exit(1)
        opts['repoid'] = repo['id']
    else:
        parser.error(_("Please specify one of: --tag, --task, --buildroot"))
        assert False
    output = koji.genMockConfig(name, arch, **opts)
    if options.ofile:
        fo = file(options.ofile, 'w')
        fo.write(output)
        fo.close()
    else:
        print output

def handle_disable_host(options, session, args):
    "[admin] Mark a host 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)
    (options, args) = parser.parse_args(args)
    if len(args) != 1:
        parser.error(_("Exactly one argument (a hostname) is required"))
        assert False
    activate_session(session)
    try:
        session.disableHost(args[0])
    except koji.GenericError, e:
        print "Could not enable host", e

def handle_enable_host(options, session, args):
    "[admin] Mark a host 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)
    (options, args) = parser.parse_args(args)
    if len(args) != 1:
        parser.error(_("Exactly one argument (a hostname) is required"))
        assert False
    activate_session(session)
    try:
        session.enableHost(args[0])
    except koji.GenericError, e:
        print "Could not enable host", e

def handle_import(options, session, args):
    "[admin] Import local RPMs to 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 the rpm"))
    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
    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
    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 = 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:
            if prev['payloadhash'] == koji.hex_string(data['sigmd5']):
                print _("RPM already imported: %s") % path
            else:
                print _("WARNING: md5sum mismatch for %s") % path
            print _("Skipping import")
            return
        if options.test:
            print _("Test mode -- skipping import for %s") % path
            return
        serverdir = _unique_path('cli-import')
        if options.link:
            old_umask = os.umask(002)
            dst = "%s/%s/%s" % (koji.pathinfo.work(), serverdir, os.path.basename(path))
            koji.ensuredir(os.path.dirname(dst))
            os.chown(os.path.dirname(dst), 48, 48)  #XXX - hack
            print "Linking rpm to: %s" % dst
            os.link(path, dst)
            os.umask(old_umask)
        else:
            print _("uploading %s...") % path,
            sys.stdout.flush()
            session.uploadWrapper(path, serverdir, blocksize=65536)
            print _("done")
            sys.stdout.flush()
        print _("importing %s...") % path,
        sys.stdout.flush()
        try:
            session.importRPM(serverdir, os.path.basename(path))
        except koji.GenericError, e:
            print _("\nError importing: %s" % str(e).splitlines()[-1])
            sys.stdout.flush()
        else:
            print _("done")
        sys.stdout.flush()

    for nvr in nvrs:
        got_build = False
        #srpms first, if any
        for path, data in to_import[nvr]:
            if data['sourcepackage']:
                do_import(path, data)
                got_build = True
        for path, data in to_import[nvr]:
            if data['sourcepackage']:
                continue
            if not got_build:
                binfo = session.getBuild(nvr)
                if binfo:
                    got_build = True
                elif options.create_build:
                    binfo = koji.parse_NVR(nvr)
                    if options.src_epoch:
                        binfo['epoch'] = options.src_epoch
                    else:
                        binfo['epoch'] = data['epoch']
                    if options.test:
                        print _("Test mode -- would have created empty build: %s") % nvr
                        got_build = True  #avoid duplicate notices
                    else:
                        print _("Creating empty build: %s") % nvr
                        session.createEmptyBuild(**binfo)
                else:
                    #shouldn't happen
                    print _("Build missing: %s") % nvr
                    break
            do_import(path, data)


# Currently disabled, needs porting to yum.comps
#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
#    comps = Comps(args[0])
#    tag = args[1]
#    force = local_options.force
#    #add all the groups first (so that group reqs do not break)
#    for name,group in comps.groups.items():
#        print "Group: %s (%s)" % (group.id,name)
#        session.groupListAdd(tag,group.id,force=force,display_name=name,
#                        is_default=bool(group.default),
#                        uservisible=bool(group.user_visible),
#                        description=group.description,
#                        langonly=group.langonly,
#                        biarchonly=bool(group.biarchonly))
#        #for k in ('id','biarchonly','langonly','user_visible','default','description'):
#        #    print "  %s: %s" %(k,getattr(group,k))
#    for name,group in comps.groups.items():
#        print "Group: %s (%s)" % (group.id,name)
#        for pkg in group.pkgs.values():
#            pkg = pkg.copy()
#            pkg_name = pkg['package']
#            if group.pkgConditionals.has_key(pkg_name):
#                pkg['requires'] = group.pkgConditionals[pkg_name]
#            pkg['basearchonly'] = bool(pkg['baseonly'])
#            del pkg['package']
#            del pkg['baseonly']
#            print "  Package: %s: %r" % (pkg_name, pkg)
#            session.groupPackageListAdd(tag,group.id,pkg_name,force=force, **pkg)
#        for type,req in group.groups.values():
#            print "  Req: %s (%s)" % (req,type)
#            session.groupReqListAdd(tag,group.id,req,force=force,type=type)
#        for type,req in group.metapkgs.values():
#            print "  Metapkg: %s (%s)" %(req,type)
#            session.groupReqListAdd(tag,group.id,req,force=force,type=type,is_metapkg=True)

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("--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
    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','sourcepackage'))
        if data['sourcepackage']:
            data['arch'] = 'src'
        sigkey = data['siggpg']
        if not sigkey:
            sigkey = ""
            if not options.with_unsigned:
                print _("Skipping unsigned package: %s" % path)
                continue
        else:
            sigkey = koji.hex_string(sigkey[13:17])
        del data['siggpg']
        rinfo = session.getRPM(data)
        if not rinfo:
            print "No such rpm in system: %(name)s-%(version)s-%(release)s.%(arch)s" % data
            continue
        sighdr = koji.rip_rpm_sighdr(path)
        previous = session.queryRPMSigs(rpm_id=rinfo['id'], sigkey=sigkey)
        assert len(previous) <= 1
        if previous:
            sighash = md5.new(sighdr).hexdigest()
            if previous[0]['sighash'] == sighash:
                print _("Signature already imported: %s") % path
                continue
            else:
                print _("Warning: signature mismatch: %s") % path
                continue
        print _("Importing signature [key %s] from %s...") % (sigkey, path)
        if not options.test:
            session.addRPMSig(rinfo['id'], base64.encodestring(sighdr))

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"))
    (options, args) = parser.parse_args(args)
    if len(args) < 1:
        parser.error(_("A signature key must be specified"))
        assert False
    if len(args) < 2 and not options.all:
        parser.error(_("At least one RPM must be specified"))
        assert False
    key = args.pop(0)
    activate_session(session)
    if options.all:
        rpms = session.queryRPMSigs(sigkey=key)
        count = 1
        for rpm in rpms:
            print "%d/%d" % (count, len(rpms))
            count += 1
            session.writeSignedRPM(rpm['rpm_id'], key)
    else:
        for nvr in args:
            build = session.getBuild(nvr)
            rpms = session.listRPMs(buildID=build['id'])
            for rpm in rpms:
                session.writeSignedRPM(rpm['id'], key)

def handle_list_permissions(options, session, args):
    "[admin] 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=_("Only list permissions for this user"))
    (options, args) = parser.parse_args(args)
    if len(args) > 0:
        parser.error(_("This command takes no arguments"))
        assert False
    activate_session(session)
    if options.user:
        user = session.getUser(options.user)
        if not user:
            raise koji.GenericError("%s can not be found" % options.user)
        perms = session.getUserPerms(user['id'])
    else:
        perms = [p['name'] for p in session.getAllPerms()]
    print perms

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 enable"))
    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)
        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
    activate_session(session)
    for nvr in args:
        data = koji.parse_NVR(nvr)
        print _("importing %s...") % nvr,
        try:
            session.importBuildInPlace(data)
        except koji.GenericError, e:
            print _("\nError importing: %s" % str(e).splitlines()[-1])
            sys.stdout.flush()
        else:
            print _("done")
        sys.stdout.flush()

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("--list", action="store_true", help=_("List possible permissions"))
    (options, args) = parser.parse_args(args)
    if not options.list and len(args) < 2:
        parser.error(_("Please specify a permission and at least one user"))
        assert False
    activate_session(session)
    perms = dict([(p['name'], p['id']) for p in session.getAllPerms()])
    if options.list:
        for p in perms.keys():
            print p
        return
    perm_id = perms.get(args[0], None)
    if perm_id is None:
        print "No such permission: %s" % args[0]
        sys.exit(1)
    names = args[1:]
    users = []
    for n in names:
        user = session.getUser(n)
        if user is None:
            print "No such user: %s" % n
            sys.exit(1)
        users.append(user)
    for user in users:
        session.grantPermission(user['id'], perm_id)

def anon_handle_latest_pkg(options, session, args):
    "Print the latest packages for a tag"
    usage = _("usage: %prog latest-pkg [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"))
    parser.add_option("--paths", action="store_true", help=_("Show the file paths"))
    (options, args) = parser.parse_args(args)
    if len(args) == 0:
        parser.error(_("A tag name must be specified"))
        assert False
    activate_session(session)
    if options.all:
        if len(args) > 1:
            parser.error(_("A package name may not be combined with --all"))
            assert False
        # 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
    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:
            data = session.getLatestBuilds(args[0], package=pkg)
            if options.paths:
                for x in data:
                    x['name'] = x['package_name']
                    x['path'] = pathinfo.build(x)
                fmt = "%(path)-40s  %(tag_name)-20s  %(owner_name)s"
            else:
                fmt = "%(nvr)-40s  %(tag_name)-20s  %(owner_name)s"
            if not options.quiet:
                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):
    "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
    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 x['args']:
            expanded = []
            for arg in x['args']:
                if type(arg) is str:
                    expanded.append(arg)
                else:
                    expanded.append('%s=%s' % (arg[0], arg[1]))
            args = ", ".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):
    "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("--quiet", action="store_true", help=_("Do not print the header information"))
    parser.add_option("--paths", action="store_true", help=_("Show the file paths"))
    parser.add_option("--sigs", action="store_true", help=_("Show signatures"))
    (options, args) = parser.parse_args(args)
    if len(args) == 0:
        parser.error(_("A tag name must be specified"))
        assert False
    elif len(args) > 2:
        parser.error(_("Only one package name may be specified"))
        assert False
    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 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.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']]
                build['name'] = build['package_name']
                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 x.has_key('path')]
        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:
            for x in data:
                x['name'] = x['package_name']
                x['path'] = pathinfo.build(x)
            fmt = "%(path)-40s  %(tag_name)-20s  %(owner_name)s"
        else:
            fmt = "%(nvr)-40s  %(tag_name)-20s  %(owner_name)s"
        if not options.quiet:
            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):
    "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"))
    (options, args) = parser.parse_args(args)
    if len(args) != 1:
        parser.error(_("Incorrect number of arguments"))
        assert False
    activate_session(session)
    package = None
    buildrootID = int(args[0])
    opts = {}
    if options.built:
        opts['buildrootID'] = buildrootID
    else:
        opts['componentBuildrootID'] = buildrootID
    data = session.listRPMs(**opts)

    fmt = "%(nvr)s"
    output = [ fmt % x for x in data]
    output.sort()
    for line in output:
        print line

def anon_handle_list_untagged(options, session, args):
    "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
    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)
        has_ref = {}
        #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 not refs.has_key(x['id'])]
    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):
    "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)
    (options, args) = parser.parse_args(args)
    if len(args) < 1 or len(args) > 2:
        parser.error(_("Incorrect number of arguments"))
        assert False
    opts = {}
    activate_session(session)
    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)
        #print "%(name)-28s %(enabled)-7s %(ready)-5s %(task_load)-4s %(capacity)-8s %(arches)s" % host

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>")
    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(_("This command takes three arguments"))
        assert False
    tag = args[0]
    group = args[1]
    pkg = args[2]
    activate_session(session)
    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(_("This command takes at least three arguments"))
        assert False
    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>")
    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(_("This command takes three arguments"))
        assert False
    tag = args[0]
    group = args[1]
    pkg = args[2]
    activate_session(session)
    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(_("This command takes three arguments"))
        assert False
    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(_("This command takes three arguments"))
        assert False
    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(_("This command takes three arguments"))
        assert False
    tag = args[0]
    group = args[1]
    req = args[2]
    activate_session(session)
    session.groupReqListUnblock(tag, group, req)

def anon_handle_list_hosts(options, session, args):
    "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"))
    (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
        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):
    "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"))
    parser.add_option("--noinherit", action="store_true", help=_("Don't follow inheritance"))
    parser.add_option("--show-blocked", action="store_true", help=_("Show blocked packages"))
    (options, args) = parser.parse_args(args)
    if len(args) != 0:
        parser.error(_("This command takes no arguments"))
        assert False
    activate_session(session)
    opts = {}
    if options.owner:
        user = session.getUser(options.owner)
        if user is None:
            parser.error(_("Invalid user"))
            assert False
        opts['userID'] = user['id']
    if options.tag:
        tag = session.getTag(options.tag)
        if tag is None:
            parser.error(_("Invalid tag"))
            assert False
        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
    opts['with_dups'] = True
    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 %-16s" % ('Package','Tag','Extra Arches','Owner')
            print "%s %s %s %s" % ('-'*23,'-'*23,'-'*16,'-'*16)
    for pkg in data:
        if allpkgs:
            print pkg['package_name']
        else:
            if not options.show_blocked and pkg.get('blocked',False):
                continue
            if pkg.has_key('tag_id'):
                if pkg['extra_arches'] is None:
                    pkg['extra_arches'] = ""
                fmt = "%(package_name)-23s %(tag_name)-23s %(extra_arches)-16s %(owner_name)-16s"
                if pkg.get('blocked',False):
                    fmt += " [BLOCKED]"
            else:
                fmt = "%(package_name)s"
            print fmt % pkg

def anon_handle_rpminfo(options, session, args):
    "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)
    (options, args) = parser.parse_args(args)
    if len(args) < 1:
        parser.error(_("Please specify an RPM"))
        assert False
    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']) + ":"
        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
        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 "Payload: %(payloadhash)s" %info
        print "Size: %(size)s" %info
        print "Build ID: %(build_id)s" %info

def anon_handle_buildinfo(options, session, args):
    "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
    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
        if info['epoch'] is None:
            info['epoch'] = ""
        else:
            info['epoch'] = str(info['epoch']) + ":"
        info['name'] = info['package_name']
        info['arch'] = 'src'
        info['state'] = koji.BUILD_STATES[info['state']]
        rpms = session.listRPMs(buildID=info['id'])
        print "BUILD: %(name)s-%(version)s-%(release)s [%(id)d]" % info
        print "State: %(state)s" % info
        print "Built by: %(owner_name)s" % info
        print "Task: %(task_id)s" % info
        print "Finished: %s" % koji.formatTimeLong(info['completion_time'])
        print "RPMs:"
        for rpm in rpms:
            print os.path.join(koji.pathinfo.build(info), koji.pathinfo.rpm(rpm))
        if options.changelog:
            print "Changelog:"
            for entry in session.getChangelogEntries(info['id']):
                print ("* %s %s" % (time.strftime('%a %b %d %Y', time.strptime(entry['date'], '%Y-%m-%d %H:%M:%S')), entry['author'])).encode('utf-8')
                print entry['text'].encode('utf-8')

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
    elif len(args) > 3:
        parser.error(_("Incorrect number of arguments"))
        assert False
    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
    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
    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
    if options.dest_tag:
        targetInfo['dest_tag_name'] = options.dest_tag

    session.editBuildTarget(targetInfo['orig_name'], targetInfo['name'], targetInfo['build_tag_name'], targetInfo['dest_tag_name'])

def handle_remove_target(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
    activate_session(session)

    if not session.hasPerm('admin'):
        print "This action requires admin privileges"
        return

    session.deleteBuildTarget(args[0])

def anon_handle_list_targets(options, session, args):
    "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"))
    (options, args) = parser.parse_args(args)
    if len(args) != 0:
        parser.error(_("This command takes no arguments"))
        assert False
    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(u'\u2502'.encode('UTF-8'))
            outdepth = depth

    sys.stdout.write(' ' * ((currtag['currdepth'] - outdepth) * 3 - 1))
    if siblings:
        sys.stdout.write(u'\u251c'.encode('UTF-8'))
    else:
        sys.stdout.write(u'\u2514'.encode('UTF-8'))
    sys.stdout.write(u'\u2500'.encode('UTF-8'))
    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):
    "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"))
    (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
    activate_session(session)
    tag = session.getTag(args[0])
    if not tag:
        parser.error(_("Unknown tag: %s" % args[0]))

    opts = {}
    opts['reverse'] = options.reverse or False
    opts['stop'] = {}
    opts['jump'] = {}

    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['jump'][str(tag1)] = tag2

    if options.stop:
        tag1 = session.getTagID(options.stop)
        if not tag1:
            parser.error(_("Unknown tag: %s" % options.stop))
        opts['stop'] = {str(tag1): 1}

    sys.stdout.write('%s (%i)\n' % (tag['name'], tag['id']))
    _printInheritance(session.getFullInheritance(tag['id'], None, opts['reverse'], opts['stop'], opts['jump']), None, opts['reverse'])

def anon_handle_list_tags(options, session, args):
    "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)
    #if len(args) != 0:
    #    parser.error(_("This command takes no arguments"))
    #    assert False
    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

    if options.build:
        buildinfo = session.getBuild(options.build)
        if not buildinfo:
            parser.error(_("Invalid build %s" % options.build))
            assert False

    tags = session.listTags(buildinfo.get('id',None), pkginfo.get('id',None))
    tags.sort(lambda a,b: cmp(a['name'],b['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:
            print fmt % tag,
            if tag['locked']:
                print ' [LOCKED]',
            if tag['perm']:
                print ' [%(perm)s perm required]' % tag,
            print ''

def anon_handle_list_tag_history(options, session, args):
    "Print a history of tag operations"
    usage = _("usage: %prog list-tag-history [options] [pattern]")
    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
    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 = "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 event_id, x in timeline:
        if options.debug:
            print "%r" % x
        print _histline(event_id, x)

def _parseTaskParams(session, method, task_id):
    """Parse the return of getTaskRequest()"""
    params = session.getTaskRequest(task_id)

    lines = []

    if method == 'buildFromCVS':
        lines.append("CVS URL: %s" % params[0])
        lines.append("Build Target: %s" % params[1])
    elif method == 'buildSRPMFromCVS':
        lines.append("CVS URL: %s" % params[0])
    elif method == 'multiArchBuild':
        lines.append("SRPM: %s/work/%s" % (options.topdir, params[0]))
        lines.append("Build Target: %s" % params[1])
        lines.append("Options:")
        for key in params[2].keys():
            if not key == '__starstar':
                lines.append("  %s: %s" % (key, params[2][key]))
    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:
            for key in params[4].keys():
                if not key == '__starstar':
                    lines.append("%s: %s" % (key, params[4][key]))
    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])
        for key in params[2].keys():
            if not key == '__starstar':
                lines.append("%s: %s" % (key, params[2][key]))
    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']))
    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]:
                lines.append("  Options:")
                subopts = subtask[2]
                for key in subopts:
                    if not key == '__starstar':
                        lines.append("    %s: %s" % (key, subopts[key]))
            lines.append("")
    
    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['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 = session.listTaskOutput(task_id)
    logs = [filename for filename in files if filename.endswith('.log')]
    output = [filename for filename in files if not filename.endswith('.log')]
    files_dir = '%s/tasks/%i' % (koji.pathinfo.work(), task_id)

    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())
    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 in logs:
            print "%s  %s/%s" % (indent, files_dir, log)
    if output:
        print "%sOutput:" % indent
        for filename in output:
            print "%s %s/%s" % (indent, files_dir, filename)

    # white space
    sys.stdout.write("\n")

    if recurse:
        level += 1
        children = session.getTaskChildren(task_id)
        for child in children:
            _printTaskInfo(session, child['id'], level, verbose=verbose)

def anon_handle_taskinfo(options, session, args):
    """Show information about a task"""
    usage = _("usage: %prog taskinfo [options] task_id")
    usage += _("\n(Specify the --help global option for a list of other help options)")
    parser = OptionParser(usage=usage)
    parser.add_option("--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(_("This command takes exctly one argument: a task ID"))
        assert False

    activate_session(session)

    task_id = int(args[0])

    _printTaskInfo(session, task_id, 0, options.recurse, options.verbose)

def anon_handle_taginfo(options, session, args):
    "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)
    (options, args) = parser.parse_args(args)
    if len(args) < 1:
        parser.error(_("Please specify a tag"))
        assert False
    activate_session(session)
    perms = dict([(p['id'], p['name']) for p in session.getAllPerms()])
    for tag in args:
        info = session.getTag(tag)
        if info is None:
            print "No such tag: %s\n" % tag
            continue
        print "Tag: %(name)s [%(id)d]" %info
        print "Arches: %(arches)s" %info
        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)
        dest_targets = session.getBuildTargets(destTagID=info['id'])
        build_targets = session.getBuildTargets(buildTagID=info['id'])
        repos = {}
        for target in dest_targets + build_targets:
            if not repos.has_key(target['build_tag']):
                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:
                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"
            print "Current repo: %s" % repos[target['build_tag']]
            print "Targets that build from this tag:"
            for target in build_targets:
                print "  %s" % target['name']
        print "Inheritance:"
        for parent in session.getInheritanceData(tag):
            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']:
                print "    maxdepth: %(maxdepth)s" % parent
            if parent['pkg_filter']:
                print "    packge filter: %(filter)s" % parent
            print

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"))
    (options, args) = parser.parse_args(args)
    if len(args) != 1:
        parser.error(_("Please specify a name for the tag"))
        assert False
    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'] = ' '.join(options.arches.replace(',',' ').split())
    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"))
    (options, args) = parser.parse_args(args)
    if len(args) != 1:
        parser.error(_("Please specify a name for the tag"))
        assert False
    activate_session(session)
    tag = args[0]
    opts = {}
    if options.arches:
        opts['arches'] = ' '.join(options.arches.replace(',',' ').split())
    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
    #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
    activate_session(session)
    pdata = session.getAllPerms()
    perms = dict([(p['id'], p['name']) for p in pdata])
    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
    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
            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

    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

    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

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

    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 and options.maxdepth.isdigit():
        new_data['maxdepth'] = int(options.maxdepth)
    if options.intransitive:
        new_data['intransitive'] = options.intransitive
    if options.noconfig:
        new_data['noconfig'] = options.noconfig
    if options.pkg_filter:
        new_data['pkg_filter'] = options.pkg_filter

    # find the data we want to edit and replace it
    index = inheritanceData.index(data)
    inheritanceData[index] = new_data
    session.setInheritanceData(tag['id'], inheritanceData)

def handle_remove_tag_inheritance(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

    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

    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):
    "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("--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
    activate_session(session)
    tag = args[0]
    groups = session.getTagGroups(tag)
    if options.comps:
        print koji.generate_comps(groups)
    elif options.spec:
        print koji.make_groups_spec(groups,name='buildgroups',buildgroup='build')
    else:
        pprint.pprint(groups)

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
    for task_id in tlist:
        session.freeTask(task_id)

def handle_cancel(options, session, args):
    "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
    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
    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_list_tasks(options, session, args):
    "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("--quiet", action="store_true", 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
    activate_session(session)
    if options.mine:
        id = session.getLoggedInUser()['id']
    else:
        id = None
    tasklist = session.taskReport(owner=id)
    #tasks are pre-sorted
    tasks = dict([(x['id'], x) for x in tasklist])
    #thread the tasks
    if not tasklist:
        print "(no tasks)"
        return
    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
    seen = {}
    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
    activate_session(session)
    arches = ' '.join(args[0].replace(',',' ').split())
    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
    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"))
    (options, args) = parser.parse_args(args)
    if len(args) < 2:
        parser.error(_("Please specify an owner and at least one package"))
        assert False
    activate_session(session)
    owner = args[0]
    user = session.getUser(owner)
    if not user:
        print "No such user: %s" % owner
        sys.exit(1)
    for package in args[1:]:
        entries = session.listPackages(pkgID=package, with_dups=True)
        if not entries:
            print "No data for package %s" % package
            continue
        for entry in entries:
            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" \
                            % (package, entry['tag_name'], entry['owner_name'], user['name'])
                    continue
                if options.verbose:
                    print "Changing owner for %s in tag %s: %s -> %s" \
                            % (package, entry['tag_name'], entry['owner_name'], user['name'])
                session.packageListSetOwner(entry['tag_id'], package, user['id'])

def anon_handle_watch_task(options, session, args):
    "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)
    (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_tasks(session,tasks)

def anon_handle_watch_logs(options, session, args):
    "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(options, 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 = {}
    method = args[0]
    taskargs = map(arg_filter,args[1:])
    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=map(arg_filter,args[1:]),
                               opts=taskopts)
    print "Created task id %d" % task_id
    if options.watch:
        watch_tasks(session,[task_id])

def handle_tag_pkg(options, session, args):
    "Apply a tag to one or more packages"
    usage = _("usage: %prog tag-pkg [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
    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 %s" % task_id
    if _running_in_bg() or options.nowait:
        return
    else:
        watch_tasks(session,tasks)

def handle_move_pkg(options, session, args):
    "'Move' one or more packages between tags"
    usage = _("usage: %prog move-pkg [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
    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 %s, moving %s" % (task_id, koji.buildLabel(build))
    if _running_in_bg() or options.nowait:
        return
    else:
        watch_tasks(session,tasks)

def handle_untag_pkg(options, session, args):
    "Remove a tag from one or more packages"
    usage = _("usage: %prog untag-pkg [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("--force", action="store_true", help=_("force operation"))
    (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
    activate_session(session)
    if options.all:
        pkgs = []
        for pkg in args[1:]:
            pkgs.extend([x['nvr'] for x in session.listTagged(args[0], package=pkg)])
    else:
        pkgs = args[1:]
    for pkg in pkgs:
        print pkg
        #XXX trap errors
        session.untagBuild(args[0], pkg, 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
    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 a built package"
    usage = _("usage: %prog download-build [options] <n-v-r | build_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("--debuginfo", action="store_true", help=_("Also download -debuginfo rpms"))
    parser.add_option("-q", "--quiet", action="store_true", help=_("Do not display progress meter"))
    (suboptions, args) = parser.parse_args(args)
    if len(args) < 1:
        parser.error(_("Please specify a package N-V-R or build ID"))
        assert False
    elif len(args) > 1:
        parser.error(_("Only a single package N-V-R or build ID may be specified"))
        assert False
    
    activate_session(session)
    build = args[0]

    if build.isdigit():
        build = int(build)
    info = session.getBuild(build)
    if info is None:
        print "No such build: %s" % build
        return 1

    arches = suboptions.arches
    if len(arches) == 0:
        arches = None
    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

    if suboptions.quiet:
        pg = None
    else:
        pg = progress.TextMeter()
    
    for rpm in rpms:
        if rpm['name'].endswith('-debuginfo') and not suboptions.debuginfo:
            continue
        
        fname = "%s-%s-%s.%s.rpm" % (rpm['name'], rpm['version'], rpm['release'], rpm['arch'])
        url = "%s/%s/%s/%s/%s/%s" % (options.pkgurl, info['package_name'], rpm['version'], rpm['release'], rpm['arch'], fname)

        file = grabber.urlopen(url, progress_obj = pg, text = "%s.%s" % (rpm['name'], rpm['arch']))

        out = os.open(fname, os.O_WRONLY|os.O_CREAT|os.O_TRUNC, 0666)
        try:
            while 1:
                buf = file.read(4096)
                if not buf:
                    break
                os.write(out, buf)
        finally:
            os.close(out)
            file.close()

def handle_help(options, session, args):
    "List available commands"
    usage = _("usage: %prog help [options]")
    usage += _("\n(Specify the --help global option for a list of other help options)")
    parser = OptionParser(usage=usage)
    parser.add_option("--admin", action="store_true", help=_("show admin commands"))
    (options, args) = parser.parse_args(args)
    list_commands(show_admin=options.admin)


def list_commands(show_admin=False):
    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 alias,handler in handlers:
        desc = handler.__doc__
        if desc.startswith('[admin] '):
            if not show_admin:
                continue
            desc = desc[8:]
        print "        %-20s %s" % (alias, desc)
    print _('(Type "koji --help" for help about global options')
    print _(' or "koji <command> --help" for help about a particular command\'s options.)')

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 activate_session(session):
    """Test and login the session is applicable"""
    global options
    if options.noauth:
        #skip authentication
        pass
    elif os.path.isfile(options.cert):
        # authenticate using SSL client cert
        session.ssl_login(options.cert, options.ca, options.serverca, proxyuser=options.runas)
    elif options.user:
        # authenticate using user/password
        session.login()
    elif sys.modules.has_key('krbV'):
        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 krbV.Krb5Error, e:
            error(_("Kerberos authentication failed: '%s' (%s)") % (e.message, e.err_code))
        except socket.error, e:
            warn(_("Could not connect to Kerberos authentication service: '%s'") % e.args[1])
    if not options.noauth and not session.logged_in:
        error(_("Error: unable to log in"))
    ensure_connection(session)
    if options.debug:
        print "successfully connected to hub"

if __name__ == "__main__":

    options, command, args = get_options()

    session_opts = {}
    for k in ('user', 'password', 'debug_xmlrpc', 'debug'):
        session_opts[k] = getattr(options,k)
    session = koji.ClientSession(options.server,session_opts)
    rv = 0
    try:
        rv = locals()[command].__call__(options, session, args)
        if not rv:
            rv = 0
    except KeyboardInterrupt:
        pass
    except SystemExit:
        rv = 1
    except:
        if options.debug:
            raise
        else:
            exctype, value = sys.exc_info()[:2]
            rv = 1
            print "%s: %s" % (exctype, value)
    try:
        session.logout()
    except:
        pass
    sys.exit(rv)
