merge koji-1.3.2 changes onto the mead branch
This commit is contained in:
commit
d620309f5e
14 changed files with 434 additions and 101 deletions
2
Makefile
2
Makefile
|
|
@ -1,6 +1,6 @@
|
|||
NAME=koji
|
||||
SPECFILE = $(firstword $(wildcard *.spec))
|
||||
SUBDIRS = hub builder koji cli docs util www
|
||||
SUBDIRS = hub builder koji cli docs util www plugins
|
||||
|
||||
ifdef DIST
|
||||
DIST_DEFINES := --define "dist $(DIST)"
|
||||
|
|
|
|||
104
cli/koji
104
cli/koji
|
|
@ -226,11 +226,12 @@ def print_task_recurse(task,depth=0):
|
|||
|
||||
class TaskWatcher(object):
|
||||
|
||||
def __init__(self,task_id,session,level=0):
|
||||
def __init__(self,task_id,session,level=0,quiet=False):
|
||||
self.id = task_id
|
||||
self.session = session
|
||||
self.info = None
|
||||
self.level = level
|
||||
self.quiet = quiet
|
||||
|
||||
#XXX - a bunch of this stuff needs to adapt to different tasks
|
||||
|
||||
|
|
@ -268,19 +269,22 @@ class TaskWatcher(object):
|
|||
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
|
||||
if not self.quiet:
|
||||
print "No such task id: %i" % self.id
|
||||
sys.exit(1)
|
||||
state = self.info['state']
|
||||
if last:
|
||||
#compare and note status changes
|
||||
laststate = last['state']
|
||||
if laststate != state:
|
||||
print "%s: %s -> %s" % (self.str(), self.display_state(last), self.display_state(self.info))
|
||||
if not self.quiet:
|
||||
print "%s: %s -> %s" % (self.str(), self.display_state(last), self.display_state(self.info))
|
||||
return True
|
||||
return False
|
||||
else:
|
||||
# First time we're seeing this task, so just show the current state
|
||||
print "%s: %s" % (self.str(), self.display_state(self.info))
|
||||
if not self.quiet:
|
||||
print "%s: %s" % (self.str(), self.display_state(self.info))
|
||||
return False
|
||||
|
||||
def is_done(self):
|
||||
|
|
@ -339,42 +343,46 @@ def display_task_results(tasks):
|
|||
# shouldn't happen
|
||||
print '%s has not completed' % task_label
|
||||
|
||||
def watch_tasks(session,tasklist):
|
||||
def watch_tasks(session,tasklist,quiet=False):
|
||||
if not tasklist:
|
||||
return
|
||||
print "Watching tasks (this may be safely interrupted)..."
|
||||
if not quiet:
|
||||
print "Watching tasks (this may be safely interrupted)..."
|
||||
rv = 0
|
||||
try:
|
||||
tasks = {}
|
||||
for task_id in tasklist:
|
||||
tasks[task_id] = TaskWatcher(task_id,session)
|
||||
tasks[task_id] = TaskWatcher(task_id,session,quiet=quiet)
|
||||
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)
|
||||
else:
|
||||
if changed:
|
||||
# task is done and state just changed
|
||||
if not quiet:
|
||||
display_tasklist_status(tasks)
|
||||
if not task.is_success():
|
||||
rv = 1
|
||||
for child in session.getTaskChildren(task_id):
|
||||
child_id = child['id']
|
||||
if not child_id in tasks.keys():
|
||||
tasks[child_id] = TaskWatcher(child_id, session, task.level + 1)
|
||||
tasks[child_id] = TaskWatcher(child_id, session, task.level + 1, quiet=quiet)
|
||||
tasks[child_id].update()
|
||||
# If we found new children, go through the list again,
|
||||
# in case they have children also
|
||||
all_done = False
|
||||
if all_done:
|
||||
print
|
||||
display_task_results(tasks)
|
||||
if not quiet:
|
||||
print
|
||||
display_task_results(tasks)
|
||||
break
|
||||
|
||||
time.sleep(1)
|
||||
except (KeyboardInterrupt):
|
||||
if tasks:
|
||||
if tasks and not quiet:
|
||||
print \
|
||||
"""Tasks still running. You can continue to watch with the 'koji watch-task' command.
|
||||
Running Tasks:
|
||||
|
|
@ -662,8 +670,12 @@ def handle_build(options, session, args):
|
|||
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",
|
||||
parser.add_option("--wait", action="store_true",
|
||||
help=_("Wait on the build, even if running in the background"))
|
||||
parser.add_option("--nowait", action="store_false", dest="wait",
|
||||
help=_("Don't wait on build"))
|
||||
parser.add_option("--quiet", action="store_true",
|
||||
help=_("Do not print the task information"), default=options.quiet)
|
||||
parser.add_option("--arch-override", help=_("Override build arches"))
|
||||
parser.add_option("--repo-id", type="int", help=_("Use a specific repo"))
|
||||
parser.add_option("--noprogress", action="store_true",
|
||||
|
|
@ -705,9 +717,10 @@ def handle_build(options, session, args):
|
|||
# try to check that source is an SRPM
|
||||
if '://' not in source:
|
||||
#treat source as an srpm and upload it
|
||||
print "Uploading srpm: %s" % source
|
||||
if not build_opts.quiet:
|
||||
print "Uploading srpm: %s" % source
|
||||
serverdir = _unique_path('cli-build')
|
||||
if _running_in_bg() or build_opts.noprogress:
|
||||
if _running_in_bg() or build_opts.noprogress or build_opts.quiet:
|
||||
callback = None
|
||||
else:
|
||||
callback = _progress_callback
|
||||
|
|
@ -715,13 +728,15 @@ def handle_build(options, session, args):
|
|||
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:
|
||||
if not build_opts.quiet:
|
||||
print "Created task:", task_id
|
||||
print "Task info: %s/taskinfo?taskID=%s" % (options.weburl, task_id)
|
||||
print repr(build_opts.wait)
|
||||
if build_opts.wait or (build_opts.wait is None and not _running_in_bg()):
|
||||
session.logout()
|
||||
return watch_tasks(session,[task_id])
|
||||
return watch_tasks(session, [task_id], quiet=build_opts.quiet)
|
||||
else:
|
||||
return
|
||||
|
||||
def handle_chain_build(options, session, args):
|
||||
# XXX - replace handle_build with this, once chain-building has gotten testing
|
||||
|
|
@ -923,12 +938,13 @@ def handle_resubmit(options, session, args):
|
|||
print "Resubmitting the following task:"
|
||||
_printTaskInfo(session, taskID, 0, False, True)
|
||||
newID = session.resubmitTask(taskID)
|
||||
print "Resubmitted task %s as new task %s" % (taskID, newID)
|
||||
if not options.quiet:
|
||||
print "Resubmitted task %s as new task %s" % (taskID, newID)
|
||||
if _running_in_bg() or options.nowait:
|
||||
return
|
||||
else:
|
||||
session.logout()
|
||||
return watch_tasks(session,[newID])
|
||||
return watch_tasks(session, [newID], quiet=options.quiet)
|
||||
|
||||
def handle_call(options, session, args):
|
||||
"[admin] Execute an arbitrary XML-RPC call"
|
||||
|
|
@ -1903,21 +1919,39 @@ def handle_grant_permission(options, session, args):
|
|||
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()])
|
||||
perm_id = perms.get(args[0], None)
|
||||
if perm_id is None:
|
||||
print "No such permission: %s" % args[0]
|
||||
return 1
|
||||
perm = args[0]
|
||||
names = args[1:]
|
||||
users = []
|
||||
for n in names:
|
||||
user = session.getUser(n)
|
||||
if user is None:
|
||||
print "No such user: %s" % n
|
||||
return 1
|
||||
parser.error(_("No such user: %s" % n))
|
||||
assert False
|
||||
users.append(user)
|
||||
for user in users:
|
||||
session.grantPermission(user['id'], perm_id)
|
||||
session.grantPermission(user['name'], perm)
|
||||
|
||||
def handle_revoke_permission(options, session, args):
|
||||
"[admin] Revoke a permission from a user"
|
||||
usage = _("usage: %prog revoke-permission <permission> <user> [<user> ...]")
|
||||
usage += _("\n(Specify the --help global option for a list of other help options)")
|
||||
parser = OptionParser(usage=usage)
|
||||
(options, args) = parser.parse_args(args)
|
||||
if len(args) < 2:
|
||||
parser.error(_("Please specify a permission and at least one user"))
|
||||
assert False
|
||||
activate_session(session)
|
||||
perm = args[0]
|
||||
names = args[1:]
|
||||
users = []
|
||||
for n in names:
|
||||
user = session.getUser(n)
|
||||
if user is None:
|
||||
parser.error(_("No such user: %s" % n))
|
||||
assert False
|
||||
users.append(user)
|
||||
for user in users:
|
||||
session.revokePermission(user['name'], perm)
|
||||
|
||||
def anon_handle_latest_pkg(options, session, args):
|
||||
"Print the latest packages for a tag"
|
||||
|
|
@ -4329,6 +4363,8 @@ def anon_handle_watch_task(options, session, args):
|
|||
usage = _("usage: %prog watch-task [options] <task id> [<task id>...]")
|
||||
usage += _("\n(Specify the --help global option for a list of other help options)")
|
||||
parser = OptionParser(usage=usage)
|
||||
parser.add_option("--quiet", action="store_true",
|
||||
help=_("Do not print the task information"), default=options.quiet)
|
||||
(options, args) = parser.parse_args(args)
|
||||
activate_session(session)
|
||||
tasks = []
|
||||
|
|
@ -4340,7 +4376,7 @@ def anon_handle_watch_task(options, session, args):
|
|||
if not tasks:
|
||||
parser.error(_("at least one task id must be specified"))
|
||||
|
||||
return watch_tasks(session, tasks)
|
||||
return watch_tasks(session, tasks, quiet=options.quiet)
|
||||
|
||||
def anon_handle_watch_logs(options, session, args):
|
||||
"Watch logs in realtime"
|
||||
|
|
|
|||
10
hub/hub.conf
10
hub/hub.conf
|
|
@ -38,8 +38,16 @@ KojiWebURL = http://kojiweb.example.com/koji
|
|||
# The domain name that will be appended to Koji usernames
|
||||
# when creating email notifications
|
||||
#EmailDomain = example.com
|
||||
# weather to send the task owner and package owner email or not on success. this still goes to watchers
|
||||
# whether to send the task owner and package owner email or not on success. this still goes to watchers
|
||||
NotifyOnSuccess = True
|
||||
## Disables all notifications
|
||||
# DisableNotifications = False
|
||||
|
||||
## Koji hub plugins
|
||||
## The path where plugins are found
|
||||
# PluginPath = /usr/lib/koji-hub-plugins
|
||||
## A space-separated list of plugins to load
|
||||
# Plugins = echo
|
||||
|
||||
## If KojiDebug is on, the hub will be /very/ verbose and will report exception
|
||||
## details to clients for anticipated errors (i.e. koji's own exceptions --
|
||||
|
|
|
|||
236
hub/kojihub.py
236
hub/kojihub.py
|
|
@ -26,6 +26,7 @@ import calendar
|
|||
import koji
|
||||
import koji.auth
|
||||
import koji.db
|
||||
import koji.plugin
|
||||
import koji.policy
|
||||
import datetime
|
||||
import errno
|
||||
|
|
@ -134,6 +135,9 @@ class Task(object):
|
|||
"""Attempt to associate the task for host, either to assign or open
|
||||
|
||||
returns True if successful, False otherwise"""
|
||||
info = self.getInfo(request=True)
|
||||
self.runCallbacks('preTaskStateChange', info, 'state', koji.TASK_STATES[newstate])
|
||||
self.runCallbacks('preTaskStateChange', info, 'host_id', host_id)
|
||||
#we use row-level locks to keep things sane
|
||||
#note the SELECT...FOR UPDATE
|
||||
task_id = self.id
|
||||
|
|
@ -170,6 +174,8 @@ class Task(object):
|
|||
q = """UPDATE task SET state=%(state)s,host_id=%(host_id)s
|
||||
WHERE id=%(task_id)s"""
|
||||
_dml(q,locals())
|
||||
self.runCallbacks('postTaskStateChange', info, 'state', koji.TASK_STATES[newstate])
|
||||
self.runCallbacks('postTaskStateChange', info, 'host_id', host_id)
|
||||
return True
|
||||
|
||||
def assign(self,host_id,force=False):
|
||||
|
|
@ -196,6 +202,9 @@ class Task(object):
|
|||
|
||||
def free(self):
|
||||
"""Free a task"""
|
||||
info = self.getInfo(request=True)
|
||||
self.runCallbacks('preTaskStateChange', info, 'state', koji.TASK_STATES['FREE'])
|
||||
self.runCallbacks('preTaskStateChange', info, 'host_id', None)
|
||||
task_id = self.id
|
||||
# access checks should be performed by calling function
|
||||
query = """SELECT state FROM task WHERE id = %(id)i FOR UPDATE"""
|
||||
|
|
@ -211,23 +220,31 @@ class Task(object):
|
|||
q = """UPDATE task SET state=%(newstate)s,host_id=%(newhost)s
|
||||
WHERE id=%(task_id)s"""
|
||||
_dml(q,locals())
|
||||
self.runCallbacks('postTaskStateChange', info, 'state', koji.TASK_STATES['FREE'])
|
||||
self.runCallbacks('postTaskStateChange', info, 'host_id', None)
|
||||
return True
|
||||
|
||||
def setWeight(self,weight):
|
||||
"""Set weight for task"""
|
||||
task_id = self.id
|
||||
weight = float(weight)
|
||||
info = self.getInfo(request=True)
|
||||
self.runCallbacks('preTaskStateChange', info, 'weight', weight)
|
||||
# access checks should be performed by calling function
|
||||
q = """UPDATE task SET weight=%(weight)s WHERE id = %(task_id)s"""
|
||||
_dml(q,locals())
|
||||
self.runCallbacks('postTaskStateChange', info, 'weight', weight)
|
||||
|
||||
def setPriority(self, priority, recurse=False):
|
||||
"""Set priority for task"""
|
||||
task_id = self.id
|
||||
priority = int(priority)
|
||||
|
||||
info = self.getInfo(request=True)
|
||||
self.runCallbacks('preTaskStateChange', info, 'priority', priority)
|
||||
# access checks should be performed by calling function
|
||||
q = """UPDATE task SET priority=%(priority)s WHERE id = %(task_id)s"""
|
||||
_dml(q,locals())
|
||||
self.runCallbacks('postTaskStateChange', info, 'priority', priority)
|
||||
|
||||
if recurse:
|
||||
"""Change priority of child tasks"""
|
||||
|
|
@ -241,11 +258,18 @@ class Task(object):
|
|||
Returns True if successful, False if not"""
|
||||
task_id = self.id
|
||||
# access checks should be performed by calling function
|
||||
st_closed = koji.TASK_STATES['CLOSED']
|
||||
# this is an approximation, and will be different than what is in the database
|
||||
# the actual value should be retrieved from the 'new' value of the post callback
|
||||
now = time.time()
|
||||
info = self.getInfo(request=True)
|
||||
self.runCallbacks('preTaskStateChange', info, 'state', state)
|
||||
self.runCallbacks('preTaskStateChange', info, 'completion_ts', now)
|
||||
update = """UPDATE task SET result = %(result)s, state = %(state)s, completion_time = NOW()
|
||||
WHERE id = %(task_id)d
|
||||
"""
|
||||
_dml(update,locals())
|
||||
self.runCallbacks('postTaskStateChange', info, 'state', state)
|
||||
self.runCallbacks('postTaskStateChange', info, 'completion_ts', now)
|
||||
|
||||
def close(self,result):
|
||||
# access checks should be performed by calling function
|
||||
|
|
@ -276,6 +300,10 @@ class Task(object):
|
|||
successfully canceled, or if it was already canceled, False if it is
|
||||
closed."""
|
||||
# access checks should be performed by calling function
|
||||
now = time.time()
|
||||
info = self.getInfo(request=True)
|
||||
self.runCallbacks('preTaskStateChange', info, 'state', koji.TASK_STATES['CANCELED'])
|
||||
self.runCallbacks('preTaskStateChange', info, 'completion_ts', now)
|
||||
task_id = self.id
|
||||
q = """SELECT state FROM task WHERE id = %(task_id)s FOR UPDATE"""
|
||||
state = _singleValue(q,locals())
|
||||
|
|
@ -289,6 +317,8 @@ class Task(object):
|
|||
update = """UPDATE task SET state = %(st_canceled)i, completion_time = NOW()
|
||||
WHERE id = %(task_id)i"""
|
||||
_dml(update, locals())
|
||||
self.runCallbacks('postTaskStateChange', info, 'state', koji.TASK_STATES['CANCELED'])
|
||||
self.runCallbacks('postTaskStateChange', info, 'completion_ts', now)
|
||||
#cancel associated builds (only if state is 'BUILDING')
|
||||
#since we check build state, we avoid loops with cancel_build on our end
|
||||
b_building = koji.BUILD_STATES['BUILDING']
|
||||
|
|
@ -403,6 +433,22 @@ class Task(object):
|
|||
task['request'] = xmlrpclib.loads(task['request'])[0]
|
||||
return results
|
||||
|
||||
def runCallbacks(self, cbtype, old_info, attr, new_val):
|
||||
if cbtype.startswith('pre'):
|
||||
info = old_info
|
||||
elif cbtype.startswith('post'):
|
||||
info = self.getInfo(request=True)
|
||||
new_val = info[attr]
|
||||
else:
|
||||
raise koji.GenericError, 'unknown callback type: %s' % cbtype
|
||||
old_val = old_info[attr]
|
||||
if attr == 'state':
|
||||
# state is passed in as an integer, but we want to use the string
|
||||
old_val = koji.TASK_STATES[old_val]
|
||||
new_val = koji.TASK_STATES[new_val]
|
||||
koji.plugin.run_callbacks(cbtype, attribute=attr, old=old_val, new=new_val,
|
||||
info=info)
|
||||
|
||||
def make_task(method,arglist,**opts):
|
||||
"""Create a task
|
||||
|
||||
|
|
@ -464,6 +510,7 @@ def make_task(method,arglist,**opts):
|
|||
allow_none=1)
|
||||
opts['state'] = koji.TASK_STATES['FREE']
|
||||
opts['method'] = method
|
||||
koji.plugin.run_callbacks('preTaskStateChange', attribute='state', old=None, new='FREE', info=opts)
|
||||
# stick it in the database
|
||||
q = """
|
||||
INSERT INTO task (state,owner,method,request,priority,
|
||||
|
|
@ -474,6 +521,8 @@ def make_task(method,arglist,**opts):
|
|||
_dml(q,opts)
|
||||
q = """SELECT currval('task_id_seq')"""
|
||||
task_id = _singleValue(q, {})
|
||||
opts['id'] = task_id
|
||||
koji.plugin.run_callbacks('postTaskStateChange', attribute='state', old=None, new='FREE', info=opts)
|
||||
return task_id
|
||||
|
||||
def mktask(__taskopts,__method,*args,**opts):
|
||||
|
|
@ -753,6 +802,13 @@ def pkglist_add(taginfo,pkginfo,owner=None,block=None,extra_arches=None,force=Fa
|
|||
pkg_id = pkg['id']
|
||||
if owner is not None:
|
||||
owner = get_user(owner,strict=True)['id']
|
||||
action = 'add'
|
||||
if update:
|
||||
action = 'update'
|
||||
elif bool(block):
|
||||
action = 'block'
|
||||
koji.plugin.run_callbacks('prePackageListChange', action=action, tag=tag, package=pkg, owner=owner,
|
||||
block=block, extra_arches=extra_arches, force=force, update=update)
|
||||
# first check to see if package is:
|
||||
# already present (via inheritance)
|
||||
# blocked
|
||||
|
|
@ -796,6 +852,8 @@ def pkglist_add(taginfo,pkginfo,owner=None,block=None,extra_arches=None,force=Fa
|
|||
else:
|
||||
raise koji.GenericError, "owner not specified"
|
||||
_pkglist_add(tag_id,pkg_id,owner,block,extra_arches)
|
||||
koji.plugin.run_callbacks('postPackageListChange', action=action, tag=tag, package=pkg, owner=owner,
|
||||
block=block, extra_arches=extra_arches, force=force, update=update)
|
||||
|
||||
def pkglist_remove(taginfo,pkginfo,force=False):
|
||||
"""Remove package from the list for tag
|
||||
|
|
@ -807,9 +865,11 @@ def pkglist_remove(taginfo,pkginfo,force=False):
|
|||
"""
|
||||
#only admins....
|
||||
context.session.assertPerm('admin')
|
||||
tag_id = get_tag_id(taginfo, strict=True)
|
||||
pkg_id = get_package_id(pkginfo, strict=True)
|
||||
_pkglist_remove(tag_id,pkg_id)
|
||||
tag = get_tag(taginfo, strict=True)
|
||||
pkg = lookup_package(pkginfo, strict=True)
|
||||
koji.plugin.run_callbacks('prePackageListChange', action='remove', tag=tag, package=pkg)
|
||||
_pkglist_remove(tag['id'],pkg['id'])
|
||||
koji.plugin.run_callbacks('postPackageListChange', action='remove', tag=tag, package=pkg)
|
||||
|
||||
def pkglist_block(taginfo,pkginfo):
|
||||
"""Block the package in tag"""
|
||||
|
|
@ -823,6 +883,7 @@ def pkglist_unblock(taginfo,pkginfo):
|
|||
the blocking entry is simply removed"""
|
||||
tag = get_tag(taginfo, strict=True)
|
||||
pkg = lookup_package(pkginfo, strict=True)
|
||||
koji.plugin.run_callbacks('prePackageListChange', action='unblock', tag=tag, package=pkg)
|
||||
tag_id = tag['id']
|
||||
pkg_id = pkg['id']
|
||||
pkglist = readPackageList(tag_id, pkgID=pkg_id, inherit=True)
|
||||
|
|
@ -845,6 +906,7 @@ def pkglist_unblock(taginfo,pkginfo):
|
|||
if not pkglist.has_key(pkg_id) or pkglist[pkg_id]['blocked']:
|
||||
_pkglist_add(tag_id,pkg_id,previous['owner_id'],False,previous['extra_arches'],
|
||||
event_id)
|
||||
koji.plugin.run_callbacks('postPackageListChange', action='unblock', tag=tag, package=pkg)
|
||||
|
||||
def pkglist_setowner(taginfo,pkginfo,owner,force=False):
|
||||
"""Set the owner for package in tag"""
|
||||
|
|
@ -1243,6 +1305,12 @@ def _tag_build(tag,build,user_id=None,force=False):
|
|||
"""
|
||||
tag = get_tag(tag, strict=True)
|
||||
build = get_build(build, strict=True)
|
||||
if user_id:
|
||||
user = get_user(user_id, strict=True)
|
||||
else:
|
||||
# use the user associated with the current session
|
||||
user = get_user(context.session.user_id, strict=True)
|
||||
koji.plugin.run_callbacks('preTag', tag=tag, build=build, user=user, force=force)
|
||||
tag_id = tag['id']
|
||||
build_id = build['id']
|
||||
nvr = "%(name)s-%(version)s-%(release)s" % build
|
||||
|
|
@ -1274,6 +1342,7 @@ def _tag_build(tag,build,user_id=None,force=False):
|
|||
q = """INSERT INTO tag_listing(tag_id,build_id,active,create_event)
|
||||
VALUES(%(tag_id)i,%(build_id)i,TRUE,%(event_id)i)"""
|
||||
_dml(q,locals())
|
||||
koji.plugin.run_callbacks('postTag', tag=tag, build=build, user=user, force=force)
|
||||
|
||||
def _untag_build(tag,build,user_id=None,strict=True,force=False):
|
||||
"""Untag a build
|
||||
|
|
@ -1286,6 +1355,12 @@ def _untag_build(tag,build,user_id=None,strict=True,force=False):
|
|||
"""
|
||||
tag = get_tag(tag, strict=True)
|
||||
build = get_build(build, strict=True)
|
||||
if user_id:
|
||||
user = get_user(user_id, strict=True)
|
||||
else:
|
||||
# use the user associated with the current session
|
||||
user = get_user(context.session.user_id, strict=True)
|
||||
koji.plugin.run_callbacks('preUntag', tag=tag, build=build, user=user, force=force, strict=strict)
|
||||
tag_id = tag['id']
|
||||
build_id = build['id']
|
||||
assert_tag_access(tag_id,user_id=user_id,force=force)
|
||||
|
|
@ -1297,6 +1372,7 @@ def _untag_build(tag,build,user_id=None,strict=True,force=False):
|
|||
if count == 0 and strict:
|
||||
nvr = "%(name)s-%(version)s-%(release)s" % build
|
||||
raise koji.TagError, "build %s not in tag %s" % (nvr,tag['name'])
|
||||
koji.plugin.run_callbacks('postUntag', tag=tag, build=build, user=user, force=force, strict=strict)
|
||||
|
||||
# tag-group operations
|
||||
# add
|
||||
|
|
@ -1870,6 +1946,8 @@ def repo_init(tag, with_src=False, with_debuginfo=False, event=None):
|
|||
logger = logging.getLogger("koji.hub.repo_init")
|
||||
state = koji.REPO_INIT
|
||||
tinfo = get_tag(tag, strict=True, event=event)
|
||||
koji.plugin.run_callbacks('preRepoInit', tag=tinfo, with_src=with_src, with_debuginfo=with_debuginfo,
|
||||
event=event, repo_id=None)
|
||||
tag_id = tinfo['id']
|
||||
repo_arches = {}
|
||||
if tinfo['arches']:
|
||||
|
|
@ -1986,6 +2064,8 @@ def repo_init(tag, with_src=False, with_debuginfo=False, event=None):
|
|||
for artifact_dir, artifacts in artifact_dirs.iteritems():
|
||||
_write_maven_repo_metadata(artifact_dir, artifacts)
|
||||
|
||||
koji.plugin.run_callbacks('postRepoInit', tag=tinfo, with_src=with_src, with_debuginfo=with_debuginfo,
|
||||
event=event, repo_id=repo_id)
|
||||
return [repo_id, event_id]
|
||||
|
||||
def _populate_maven_repodir(buildinfo, maveninfo, archiveinfo, repodir, artifact_dirs):
|
||||
|
|
@ -3612,6 +3692,8 @@ def new_build(data):
|
|||
row = _fetchSingle(q, data)
|
||||
if row:
|
||||
id, state, task_id = row
|
||||
data['id'] = id
|
||||
koji.plugin.run_callbacks('preBuildStateChange', attribute='state', old=state, new=data['state'], info=data)
|
||||
st_desc = koji.BUILD_STATES[state]
|
||||
if st_desc == 'BUILDING':
|
||||
# check to see if this is the controlling task
|
||||
|
|
@ -3625,22 +3707,25 @@ def new_build(data):
|
|||
update = """UPDATE build SET state=%(state)i,task_id=%(task_id)s,
|
||||
owner=%(owner)s,completion_time=%(completion_time)s,create_event=get_event()
|
||||
WHERE id = %(id)i"""
|
||||
data['id'] = id
|
||||
_dml(update, data)
|
||||
koji.plugin.run_callbacks('postBuildStateChange', attribute='state', old=state, new=data['state'], info=data)
|
||||
return id
|
||||
raise koji.GenericError, "Build already exists (id=%d, state=%s): %r" \
|
||||
% (id, st_desc, data)
|
||||
else:
|
||||
koji.plugin.run_callbacks('preBuildStateChange', attribute='state', old=None, new=data['state'], info=data)
|
||||
#insert the new data
|
||||
data['id'] = _singleValue("SELECT nextval('build_id_seq')")
|
||||
q="""
|
||||
INSERT INTO build (pkg_id,version,release,epoch,state,
|
||||
INSERT INTO build (id,pkg_id,version,release,epoch,state,
|
||||
task_id,owner,completion_time)
|
||||
VALUES (%(pkg_id)s,%(version)s,%(release)s,%(epoch)s,
|
||||
VALUES (%(id)i,%(pkg_id)i,%(version)s,%(release)s,%(epoch)s,
|
||||
%(state)s,%(task_id)s,%(owner)s,%(completion_time)s)
|
||||
"""
|
||||
_dml(q, data)
|
||||
koji.plugin.run_callbacks('postBuildStateChange', attribute='state', old=None, new=data['state'], info=data)
|
||||
#return build_id
|
||||
q="""SELECT currval('build_id_seq')"""
|
||||
return _singleValue(q)
|
||||
return data['id']
|
||||
|
||||
def check_noarch_rpms(basepath, rpms):
|
||||
"""
|
||||
|
|
@ -3682,6 +3767,8 @@ def import_build(srpm, rpms, brmap=None, task_id=None, build_id=None, logs=None)
|
|||
"""
|
||||
if brmap is None:
|
||||
brmap = {}
|
||||
koji.plugin.run_callbacks('preImport', type='build', srpm=srpm, rpms=rpms, brmap=brmap,
|
||||
task_id=task_id, build_id=build_id, build=None, logs=logs)
|
||||
uploadpath = koji.pathinfo.work()
|
||||
#verify files exist
|
||||
for relpath in [srpm] + rpms:
|
||||
|
|
@ -3709,9 +3796,12 @@ def import_build(srpm, rpms, brmap=None, task_id=None, build_id=None, logs=None)
|
|||
build['task_id'] = task_id
|
||||
if build_id is None:
|
||||
build_id = new_build(build)
|
||||
binfo = get_build(build_id, strict=True)
|
||||
else:
|
||||
#build_id was passed in - sanity check
|
||||
binfo = get_build(build_id)
|
||||
binfo = get_build(build_id, strict=True)
|
||||
st_complete = koji.BUILD_STATES['COMPLETE']
|
||||
koji.plugin.run_callbacks('preBuildStateChange', attribute='state', old=binfo['state'], new=st_complete, info=binfo)
|
||||
for key in ('name','version','release','epoch','task_id'):
|
||||
if build[key] != binfo[key]:
|
||||
raise koji.GenericError, "Unable to complete build: %s mismatch (build: %s, rpm: %s)" % (key, binfo[key], build[key])
|
||||
|
|
@ -3719,10 +3809,10 @@ def import_build(srpm, rpms, brmap=None, task_id=None, build_id=None, logs=None)
|
|||
raise koji.GenericError, "Unable to complete build: state is %s" \
|
||||
% koji.BUILD_STATES[binfo['state']]
|
||||
#update build state
|
||||
st_complete = koji.BUILD_STATES['COMPLETE']
|
||||
update = """UPDATE build SET state=%(st_complete)i,completion_time=NOW()
|
||||
WHERE id=%(build_id)i"""
|
||||
_dml(update,locals())
|
||||
koji.plugin.run_callbacks('postBuildStateChange', attribute='state', old=binfo['state'], new=st_complete, info=binfo)
|
||||
build['id'] = build_id
|
||||
# now to handle the individual rpms
|
||||
for relpath in [srpm] + rpms:
|
||||
|
|
@ -3737,6 +3827,8 @@ def import_build(srpm, rpms, brmap=None, task_id=None, build_id=None, logs=None)
|
|||
for relpath in files:
|
||||
fn = "%s/%s" % (uploadpath,relpath)
|
||||
import_build_log(fn, build, subdir=key)
|
||||
koji.plugin.run_callbacks('postImport', type='build', srpm=srpm, rpms=rpms, brmap=brmap,
|
||||
task_id=task_id, build_id=build_id, build=binfo, logs=logs)
|
||||
return build
|
||||
|
||||
def import_rpm(fn,buildinfo=None,brootid=None):
|
||||
|
|
@ -3801,6 +3893,9 @@ def import_rpm(fn,buildinfo=None,brootid=None):
|
|||
rpminfo['size'] = os.path.getsize(fn)
|
||||
rpminfo['payloadhash'] = koji.hex_string(hdr[rpm.RPMTAG_SIGMD5])
|
||||
rpminfo['brootid'] = brootid
|
||||
|
||||
koji.plugin.run_callbacks('preImport', type='rpm', rpm=rpminfo, build=buildinfo)
|
||||
|
||||
q = """INSERT INTO rpminfo (id,name,version,release,epoch,
|
||||
build_id,arch,buildtime,buildroot_id,
|
||||
external_repo_id,
|
||||
|
|
@ -3812,6 +3907,8 @@ def import_rpm(fn,buildinfo=None,brootid=None):
|
|||
"""
|
||||
_dml(q, rpminfo)
|
||||
|
||||
koji.plugin.run_callbacks('postImport', type='rpm', rpm=rpminfo, build=buildinfo)
|
||||
|
||||
return rpminfo
|
||||
|
||||
def add_external_rpm(rpminfo, external_repo, strict=True):
|
||||
|
|
@ -3952,6 +4049,7 @@ def import_build_in_place(build):
|
|||
raise koji.GenericError, "srpm mismatch for %s: %s (expected %s)" \
|
||||
% (fn,sourcerpm,srpmname)
|
||||
rpms.append(fn)
|
||||
koji.plugin.run_callbacks('preImport', type='build', in_place=True, srpm=srpm, rpms=rpms)
|
||||
# actually import
|
||||
buildinfo = None
|
||||
if srpm is not None:
|
||||
|
|
@ -3965,9 +4063,12 @@ def import_build_in_place(build):
|
|||
#update build state
|
||||
build_id = buildinfo['id']
|
||||
st_complete = koji.BUILD_STATES['COMPLETE']
|
||||
koji.plugin.run_callbacks('preBuildStateChange', attribute='state', old=buildinfo['state'], new=st_complete, info=buildinfo)
|
||||
update = """UPDATE build SET state=%(st_complete)i,completion_time=NOW()
|
||||
WHERE id=%(build_id)i"""
|
||||
_dml(update,locals())
|
||||
koji.plugin.run_callbacks('postBuildStateChange', attribute='state', old=buildinfo['state'], new=st_complete, info=buildinfo)
|
||||
koji.plugin.run_callbacks('postImport', type='build', in_place=True, srpm=srpm, rpms=rpms)
|
||||
return build_id
|
||||
|
||||
def _import_wrapper(task_id, build_info, rpm_results):
|
||||
|
|
@ -4603,6 +4704,8 @@ def _delete_build(binfo):
|
|||
# archiveinfo KEEP
|
||||
# buildroot_archives KEEP (but should ideally be empty anyway)
|
||||
# files on disk: DELETE
|
||||
st_deleted = koji.BUILD_STATES['DELETED']
|
||||
koji.plugin.run_callbacks('preBuildStateChange', attribute='state', old=binfo['state'], new=st_deleted, info=binfo)
|
||||
build_id = binfo['id']
|
||||
q = """SELECT id FROM rpminfo WHERE build_id=%(build_id)i"""
|
||||
rpm_ids = _fetchMulti(q, locals())
|
||||
|
|
@ -4613,7 +4716,6 @@ def _delete_build(binfo):
|
|||
update = """UPDATE tag_listing SET revoke_event=%(event_id)i, active=NULL
|
||||
WHERE active = TRUE AND build_id=%(build_id)i"""
|
||||
_dml(update, locals())
|
||||
st_deleted = koji.BUILD_STATES['DELETED']
|
||||
update = """UPDATE build SET state=%(st_deleted)i WHERE id=%(build_id)i"""
|
||||
_dml(update, locals())
|
||||
#now clear the build dirs
|
||||
|
|
@ -4621,9 +4723,6 @@ def _delete_build(binfo):
|
|||
builddir = koji.pathinfo.build(binfo)
|
||||
if os.path.exists(builddir):
|
||||
dirs_to_clear.append(builddir)
|
||||
archivedir = koji.pathinfo.archive(binfo)
|
||||
if os.path.exists(archivedir):
|
||||
dirs_to_clear.append(archivedir)
|
||||
maven_info = get_maven_build(build_id)
|
||||
if maven_info:
|
||||
mavendir = koji.pathinfo.mavenbuild(binfo, maven_info)
|
||||
|
|
@ -4634,7 +4733,10 @@ def _delete_build(binfo):
|
|||
if rv != 0:
|
||||
raise koji.GenericError, 'file removal failed (code %r) for %s' % (rv, filedir)
|
||||
#and clear out the emptied dirs
|
||||
os.system(r"find '%s' -xdev -depth -type d -print0 |xargs -0 rmdir" % filedir)
|
||||
rv = os.system(r"find '%s' -xdev -depth -type d -print0 |xargs -0 rmdir" % filedir)
|
||||
if rv != 0:
|
||||
raise koji.GenericError, 'directory removal failed (code %r) for %s' % (rv, filedir)
|
||||
koji.plugin.run_callbacks('postBuildStateChange', attribute='state', old=binfo['state'], new=st_deleted, info=binfo)
|
||||
|
||||
def reset_build(build):
|
||||
"""Reset a build so that it can be reimported
|
||||
|
|
@ -4658,6 +4760,7 @@ def reset_build(build):
|
|||
#nothing to do
|
||||
return
|
||||
minfo = get_maven_build(binfo)
|
||||
koji.plugin.run_callbacks('preBuildStateChange', attribute='state', old=binfo['state'], new=koji.BUILD_STATES['CANCELED'], info=binfo)
|
||||
q = """SELECT id FROM rpminfo WHERE build_id=%(id)i"""
|
||||
ids = _fetchMulti(q, binfo)
|
||||
for (rpm_id,) in ids:
|
||||
|
|
@ -4686,9 +4789,6 @@ def reset_build(build):
|
|||
builddir = koji.pathinfo.build(binfo)
|
||||
if os.path.exists(builddir):
|
||||
dirs_to_clear.append(builddir)
|
||||
archivedir = koji.pathinfo.archive(binfo)
|
||||
if os.path.exists(archivedir):
|
||||
dirs_to_clear.append(archivedir)
|
||||
if minfo:
|
||||
mavendir = koji.pathinfo.mavenbuild(binfo, minfo)
|
||||
if os.path.exists(mavendir):
|
||||
|
|
@ -4698,7 +4798,10 @@ def reset_build(build):
|
|||
if rv != 0:
|
||||
raise koji.GenericError, 'file removal failed (code %r) for %s' % (rv, filedir)
|
||||
#and clear out the emptied dirs
|
||||
os.system(r"find '%s' -xdev -depth -type d -print0 |xargs -0 rmdir" % filedir)
|
||||
rv = os.system(r"find '%s' -xdev -depth -type d -print0 |xargs -0 rmdir" % filedir)
|
||||
if rv != 0:
|
||||
raise koji.GenericError, 'directory removal failed (code %r) for %s' % (rv, filedir)
|
||||
koji.plugin.run_callbacks('postBuildStateChange', attribute='state', old=binfo['state'], new=koji.BUILD_STATES['CANCELED'], info=binfo)
|
||||
|
||||
def cancel_build(build_id, cancel_task=True):
|
||||
"""Cancel a build
|
||||
|
|
@ -4714,6 +4817,10 @@ def cancel_build(build_id, cancel_task=True):
|
|||
"""
|
||||
st_canceled = koji.BUILD_STATES['CANCELED']
|
||||
st_building = koji.BUILD_STATES['BUILDING']
|
||||
build = get_build(build_id, strict=True)
|
||||
if build['state'] != st_building:
|
||||
return False
|
||||
koji.plugin.run_callbacks('preBuildStateChange', attribute='state', old=build['state'], new=st_canceled, info=build)
|
||||
update = """UPDATE build
|
||||
SET state = %(st_canceled)i, completion_time = NOW()
|
||||
WHERE id = %(build_id)i AND state = %(st_building)i"""
|
||||
|
|
@ -4726,6 +4833,7 @@ def cancel_build(build_id, cancel_task=True):
|
|||
build_notification(task_id, build_id)
|
||||
if cancel_task:
|
||||
Task(task_id).cancelFull(strict=False)
|
||||
koji.plugin.run_callbacks('postBuildStateChange', attribute='state', old=build['state'], new=st_canceled, info=build)
|
||||
return True
|
||||
|
||||
def _get_build_target(task_id):
|
||||
|
|
@ -5425,24 +5533,20 @@ def importImageInternal(task_id, filename, filesize, arch, mediatype, hash, rpml
|
|||
exceeds the 32-bit signed integer limit. This function will convert it if
|
||||
need be. Not called for scratch images.
|
||||
"""
|
||||
|
||||
#sanity checks
|
||||
host = Host()
|
||||
host.verify()
|
||||
task = Task(task_id)
|
||||
task.assertHost(host.id)
|
||||
|
||||
imageinfo = {}
|
||||
imageinfo['id'] = _singleValue("""SELECT nextval('imageinfo_id_seq')""")
|
||||
imageinfo['taskid'] = task_id
|
||||
imageinfo['task_id'] = task_id
|
||||
imageinfo['filename'] = filename
|
||||
imageinfo['filesize'] = int(filesize)
|
||||
imageinfo['arch'] = arch
|
||||
imageinfo['mediatype'] = mediatype
|
||||
imageinfo['hash'] = hash
|
||||
|
||||
koji.plugin.run_callbacks('preImport', type='image', image=imageinfo)
|
||||
|
||||
q = """INSERT INTO imageinfo (id,task_id,filename,filesize,
|
||||
arch,mediatype,hash)
|
||||
VALUES (%(id)i,%(taskid)i,%(filename)s,%(filesize)i,
|
||||
VALUES (%(id)i,%(task_id)i,%(filename)s,%(filesize)i,
|
||||
%(arch)s,%(mediatype)s,%(hash)s)
|
||||
"""
|
||||
_dml(q, imageinfo)
|
||||
|
|
@ -5463,6 +5567,8 @@ def importImageInternal(task_id, filename, filesize, arch, mediatype, hash, rpml
|
|||
for rpm_id in rpm_ids:
|
||||
_dml(q, locals())
|
||||
|
||||
koji.plugin.run_callbacks('postImport', type='image', image=imageinfo)
|
||||
|
||||
return image_id
|
||||
|
||||
def moveImageResults(task_id, image_id, arch):
|
||||
|
|
@ -6465,7 +6571,9 @@ class RootExports(object):
|
|||
- owner_name
|
||||
- creation_event_id
|
||||
- creation_time
|
||||
- creation_ts
|
||||
- completion_time
|
||||
- completion_ts
|
||||
- task_id
|
||||
|
||||
If type == 'maven', each map will also contain the following keys:
|
||||
|
|
@ -6479,6 +6587,8 @@ class RootExports(object):
|
|||
fields = [('build.id', 'build_id'), ('build.version', 'version'), ('build.release', 'release'),
|
||||
('build.epoch', 'epoch'), ('build.state', 'state'), ('build.completion_time', 'completion_time'),
|
||||
('events.id', 'creation_event_id'), ('events.time', 'creation_time'), ('build.task_id', 'task_id'),
|
||||
('EXTRACT(EPOCH FROM events.time)','creation_ts'),
|
||||
('EXTRACT(EPOCH FROM build.completion_time)','completion_ts'),
|
||||
('package.id', 'package_id'), ('package.name', 'package_name'), ('package.name', 'name'),
|
||||
("package.name || '-' || build.version || '-' || build.release", 'nvr'),
|
||||
('users.id', 'owner_id'), ('users.name', 'owner_name')]
|
||||
|
|
@ -6790,16 +6900,20 @@ class RootExports(object):
|
|||
|
||||
results = []
|
||||
hdr = koji.get_rpm_header(rpm_path)
|
||||
fields = koji.get_header_fields(hdr, ['filenames', 'filemd5s', 'filesizes', 'fileflags'])
|
||||
fields = koji.get_header_fields(hdr, ['filenames', 'filemd5s', 'filesizes', 'fileflags',
|
||||
'fileusername', 'filegroupname', 'filemtimes', 'filemodes'])
|
||||
digest_algo = koji.util.filedigestAlgo(hdr)
|
||||
|
||||
for (name, digest, size, flags) in zip(fields['filenames'], fields['filemd5s'],
|
||||
fields['filesizes'], fields['fileflags']):
|
||||
for (name, digest, size, flags, user, group, mtime, mode) in zip(fields['filenames'], fields['filemd5s'],
|
||||
fields['filesizes'], fields['fileflags'],
|
||||
fields['fileusername'], fields['filegroupname'],
|
||||
fields['filemtimes'], fields['filemodes']):
|
||||
if queryOpts.get('asList'):
|
||||
results.append([name, digest, size, flags, digest_algo])
|
||||
results.append([name, digest, size, flags, digest_algo, user, group, mtime, mode])
|
||||
else:
|
||||
results.append({'name': name, 'digest': digest, 'digest_algo': digest_algo,
|
||||
'md5': digest, 'size': size, 'flags': flags})
|
||||
'md5': digest, 'size': size, 'flags': flags,
|
||||
'user': user, 'group': group, 'mtime': mtime, 'mode': mode})
|
||||
|
||||
return _applyQueryOpts(results, queryOpts)
|
||||
|
||||
|
|
@ -6827,7 +6941,8 @@ class RootExports(object):
|
|||
|
||||
hdr = koji.get_rpm_header(rpm_path)
|
||||
# use filemd5s for backward compatibility
|
||||
fields = koji.get_header_fields(hdr, ['filenames', 'filemd5s', 'filesizes', 'fileflags'])
|
||||
fields = koji.get_header_fields(hdr, ['filenames', 'filemd5s', 'filesizes', 'fileflags',
|
||||
'fileusername', 'filegroupname', 'filemtimes', 'filemodes'])
|
||||
digest_algo = koji.util.filedigestAlgo(hdr)
|
||||
|
||||
i = 0
|
||||
|
|
@ -6835,7 +6950,9 @@ class RootExports(object):
|
|||
if name == filename:
|
||||
return {'rpm_id': rpm_info['id'], 'name': name, 'digest': fields['filemd5s'][i],
|
||||
'digest_algo': digest_algo, 'md5': fields['filemd5s'][i],
|
||||
'size': fields['filesizes'][i], 'flags': fields['fileflags'][i]}
|
||||
'size': fields['filesizes'][i], 'flags': fields['fileflags'][i],
|
||||
'user': fields['fileusername'][i], 'group': fields['filegroupname'][i],
|
||||
'mtime': fields['filemtimes'][i], 'mode': fields['filemodes'][i]}
|
||||
i += 1
|
||||
return {}
|
||||
|
||||
|
|
@ -6981,6 +7098,19 @@ class RootExports(object):
|
|||
VALUES (%(user_id)i, %(perm_id)i)"""
|
||||
_dml(insert, locals())
|
||||
|
||||
def revokePermission(self, userinfo, permission):
|
||||
"""Revoke a permission from a user"""
|
||||
context.session.assertPerm('admin')
|
||||
user_id = get_user(userinfo, strict=True)['id']
|
||||
perm = lookup_perm(permission, strict=True)
|
||||
perm_id = perm['id']
|
||||
if perm['name'] not in koji.auth.get_user_perms(user_id):
|
||||
raise koji.GenericError, 'user %s does not have permission: %s' % (userinfo, perm['name'])
|
||||
update = """UPDATE user_perms
|
||||
SET active = NULL, revoke_event = get_event()
|
||||
WHERE user_id = %(user_id)i and perm_id = %(perm_id)i"""
|
||||
_dml(update, locals())
|
||||
|
||||
def createUser(self, username, status=None, krb_principal=None):
|
||||
"""Add a user to the database"""
|
||||
context.session.assertPerm('admin')
|
||||
|
|
@ -7478,8 +7608,10 @@ class RootExports(object):
|
|||
raise koji.GenericError, 'user does not exist: %s' % user
|
||||
userid = userinfo['id']
|
||||
buildid = buildinfo['id']
|
||||
koji.plugin.run_callbacks('preBuildStateChange', attribute='owner_id', old=buildinfo['owner_id'], new=userid, info=buildinfo)
|
||||
q = """UPDATE build SET owner=%(userid)i WHERE id=%(buildid)i"""
|
||||
_dml(q,locals())
|
||||
koji.plugin.run_callbacks('postBuildStateChange', attribute='owner_id', old=buildinfo['owner_id'], new=userid, info=buildinfo)
|
||||
|
||||
def setBuildTimestamp(self, build, ts):
|
||||
"""Set the completion time for a build
|
||||
|
|
@ -7500,11 +7632,13 @@ class RootExports(object):
|
|||
raise koji.GenericError, "Invalid time: %s" % ts
|
||||
elif not isinstance(ts, (int, long, float)):
|
||||
raise koji.GenericError, "Invalid type for timestamp"
|
||||
koji.plugin.run_callbacks('preBuildStateChange', attribute='completion_ts', old=buildinfo['completion_ts'], new=ts, info=buildinfo)
|
||||
buildid = buildinfo['id']
|
||||
q = """UPDATE build
|
||||
SET completion_time=TIMESTAMP 'epoch' AT TIME ZONE 'utc' + '%(ts)f seconds'::interval
|
||||
WHERE id=%%(buildid)i""" % locals()
|
||||
_dml(q,locals())
|
||||
koji.plugin.run_callbacks('postBuildStateChange', attribute='completion_ts', old=buildinfo['completion_ts'], new=ts, info=buildinfo)
|
||||
|
||||
def count(self, methodName, *args, **kw):
|
||||
"""Execute the XML-RPC method with the given name and count the results.
|
||||
|
|
@ -7683,7 +7817,7 @@ class RootExports(object):
|
|||
"""Search for an item in the database matching "terms".
|
||||
"type" specifies what object type to search for, and must be
|
||||
one of "package", "build", "tag", "target", "user", "host",
|
||||
"rpm", or "file". "matchType" specifies the type of search to
|
||||
or "rpm". "matchType" specifies the type of search to
|
||||
perform, and must be one of "glob" or "regexp". All searches
|
||||
are case-insensitive. A list of maps containing "id" and
|
||||
"name" will be returned. If no matches are found, an empty
|
||||
|
|
@ -7716,10 +7850,6 @@ class RootExports(object):
|
|||
elif type == 'rpm':
|
||||
clause = "name || '-' || version || '-' || release || '.' || arch || '.rpm' %s %%(terms)s" % oper
|
||||
cols = ('id', "name || '-' || version || '-' || release || '.' || arch || '.rpm'")
|
||||
elif type == 'file':
|
||||
# only search for exact matches against files, so we can use the index and not thrash the disks
|
||||
clause = 'filename = %(terms)s'
|
||||
cols = ('rpm_id', 'filename')
|
||||
elif type == 'tag':
|
||||
joins.append('tag_config ON tag.id = tag_config.tag_id')
|
||||
clause = 'tag_config.active = TRUE and name %s %%(terms)s' % oper
|
||||
|
|
@ -8425,28 +8555,30 @@ class HostExports(object):
|
|||
task = Task(task_id)
|
||||
task.assertHost(host.id)
|
||||
|
||||
st_failed = koji.BUILD_STATES['FAILED']
|
||||
buildinfo = get_build(build_id, strict=True)
|
||||
koji.plugin.run_callbacks('preBuildStateChange', attribute='state', old=buildinfo['state'], new=st_failed, info=buildinfo)
|
||||
|
||||
query = """SELECT state, completion_time
|
||||
FROM build
|
||||
WHERE id = %(build_id)i
|
||||
FOR UPDATE"""
|
||||
result = _singleRow(query, locals(), ('state', 'completion_time'))
|
||||
|
||||
if not result:
|
||||
raise koji.GenericError, 'no build with ID: %i' % build_id
|
||||
elif result['state'] != koji.BUILD_STATES['BUILDING']:
|
||||
if result['state'] != koji.BUILD_STATES['BUILDING']:
|
||||
raise koji.GenericError, 'cannot update build %i, state: %s' % \
|
||||
(build_id, koji.BUILD_STATES[result['state']])
|
||||
elif result['completion_time'] is not None:
|
||||
raise koji.GenericError, 'cannot update build %i, completed at %s' % \
|
||||
(build_id, result['completion_time'])
|
||||
|
||||
state = koji.BUILD_STATES['FAILED']
|
||||
update = """UPDATE build
|
||||
SET state = %(state)i,
|
||||
SET state = %(st_failed)i,
|
||||
completion_time = NOW()
|
||||
WHERE id = %(build_id)i"""
|
||||
_dml(update, locals())
|
||||
build_notification(task_id, build_id)
|
||||
koji.plugin.run_callbacks('postBuildStateChange', attribute='state', old=buildinfo['state'], new=st_failed, info=buildinfo)
|
||||
|
||||
def tagBuild(self,task_id,tag,build,force=False,fromtag=None):
|
||||
"""Tag a build (host version)
|
||||
|
|
@ -8479,6 +8611,12 @@ class HostExports(object):
|
|||
|
||||
# Called from kojid::LiveCDTask
|
||||
def importImage(self, task_id, filename, filesize, arch, mediatype, hash, rpmlist):
|
||||
"""Import a built image, populating the database with metadata and moving the image
|
||||
to its final location."""
|
||||
host = Host()
|
||||
host.verify()
|
||||
task = Task(task_id)
|
||||
task.assertHost(host.id)
|
||||
image_id = importImageInternal(task_id, filename, filesize, arch, mediatype,
|
||||
hash, rpmlist)
|
||||
moveImageResults(task_id, image_id, arch)
|
||||
|
|
@ -8673,6 +8811,7 @@ class HostExports(object):
|
|||
host = Host()
|
||||
host.verify()
|
||||
rinfo = repo_info(repo_id, strict=True)
|
||||
koji.plugin.run_callbacks('preRepoDone', repo=rinfo, data=data, expire=expire)
|
||||
if rinfo['state'] != koji.REPO_INIT:
|
||||
raise koji.GenericError, "Repo %(id)s not in INIT state (got %(state)s)" % rinfo
|
||||
repodir = koji.pathinfo.repo(repo_id, rinfo['tag_name'])
|
||||
|
|
@ -8692,6 +8831,7 @@ class HostExports(object):
|
|||
os.unlink(src)
|
||||
if expire:
|
||||
repo_expire(repo_id)
|
||||
koji.plugin.run_callbacks('postRepoDone', repo=rinfo, data=data, expire=expire)
|
||||
return
|
||||
#else:
|
||||
repo_ready(repo_id)
|
||||
|
|
@ -8706,7 +8846,7 @@ class HostExports(object):
|
|||
except OSError:
|
||||
#making this link is nonessential
|
||||
log_error("Unable to create latest link for repo: %s" % repodir)
|
||||
|
||||
koji.plugin.run_callbacks('postRepoDone', repo=rinfo, data=data, expire=expire)
|
||||
|
||||
def isEnabled(self):
|
||||
host = Host()
|
||||
|
|
|
|||
|
|
@ -102,12 +102,16 @@ class HandlerRegistry(object):
|
|||
if isinstance(v, (types.ClassType, types.TypeType)):
|
||||
#skip classes
|
||||
continue
|
||||
if callable(v) and getattr(v, 'exported', False):
|
||||
if hasattr(v, 'export_alias'):
|
||||
name = getattr(v, 'export_alias')
|
||||
else:
|
||||
name = v.__name__
|
||||
self.register_function(v, name=name)
|
||||
if callable(v):
|
||||
if getattr(v, 'exported', False):
|
||||
if hasattr(v, 'export_alias'):
|
||||
name = getattr(v, 'export_alias')
|
||||
else:
|
||||
name = v.__name__
|
||||
self.register_function(v, name=name)
|
||||
if getattr(v, 'callbacks', None):
|
||||
for cbtype in v.callbacks:
|
||||
koji.plugin.register_callback(cbtype, v)
|
||||
|
||||
def list_api(self):
|
||||
funcs = []
|
||||
|
|
|
|||
21
koji.spec
21
koji.spec
|
|
@ -8,7 +8,7 @@
|
|||
%define release %{baserelease}
|
||||
%endif
|
||||
Name: koji
|
||||
Version: 1.3.1
|
||||
Version: 1.3.2
|
||||
Release: %{release}%{?dist}
|
||||
License: LGPLv2 and GPLv2+
|
||||
# koji.ssl libs (from plague) are GPLv2+
|
||||
|
|
@ -41,6 +41,16 @@ Requires: %{name} = %{version}-%{release}
|
|||
%description hub
|
||||
koji-hub is the XMLRPC interface to the koji database
|
||||
|
||||
%package hub-plugins
|
||||
Summary: Koji hub plugins
|
||||
Group: Applications/Internet
|
||||
License: LGPLv2
|
||||
Requires: %{name} = %{version}-%{release}
|
||||
Requires: %{name}-hub = %{version}-%{release}
|
||||
|
||||
%description hub-plugins
|
||||
Plugins to the koji XMLRPC interface
|
||||
|
||||
%package builder
|
||||
Summary: Koji RPM builder daemon
|
||||
Group: Applications/System
|
||||
|
|
@ -124,6 +134,11 @@ rm -rf $RPM_BUILD_ROOT
|
|||
%config(noreplace) /etc/httpd/conf.d/kojihub.conf
|
||||
%config(noreplace) /etc/koji-hub/hub.conf
|
||||
|
||||
%files hub-plugins
|
||||
%defattr(-,root,root)
|
||||
%dir %{_prefix}/lib/koji-hub-plugins
|
||||
%{_prefix}/lib/koji-hub-plugins/*.py*
|
||||
|
||||
%files utils
|
||||
%defattr(-,root,root)
|
||||
%{_sbindir}/kojira
|
||||
|
|
@ -179,6 +194,10 @@ if [ $1 = 0 ]; then
|
|||
fi
|
||||
|
||||
%changelog
|
||||
* Tue Nov 10 2009 Mike Bonnet <mikeb@redhat.com> - 1.3.2-1
|
||||
- support for LiveCD creation
|
||||
- new event-based callback system
|
||||
|
||||
* Fri Jun 12 2009 Mike Bonnet <mikeb@redhat.com> - 1.3.1-2
|
||||
- use <mirrorOf>*</mirrorOf> now that Maven 2.0.8 is available in the buildroots
|
||||
- retrieve Maven info for a build from the top-level pom.xml in the source tree
|
||||
|
|
|
|||
|
|
@ -285,6 +285,14 @@ class LiveCDError(GenericError):
|
|||
"""Raised when LiveCD Image creation fails"""
|
||||
faultCode = 1015
|
||||
|
||||
class PluginError(GenericError):
|
||||
"""Raised when there is an error with a plugin"""
|
||||
faultCode = 1016
|
||||
|
||||
class CallbackError(PluginError):
|
||||
"""Raised when there is an error executing a callback"""
|
||||
faultCode = 1017
|
||||
|
||||
class MultiCallInProgress(object):
|
||||
"""
|
||||
Placeholder class to be returned by method calls when in the process of
|
||||
|
|
|
|||
|
|
@ -459,17 +459,13 @@ class Session(object):
|
|||
c.execute(update, locals())
|
||||
context.cnx.commit()
|
||||
|
||||
def createSession(self, user_id, hostip, authtype, master=None, locked=False):
|
||||
def createSession(self, user_id, hostip, authtype, master=None):
|
||||
"""Create a new session for the given user.
|
||||
|
||||
Return a map containing the session-id and session-key.
|
||||
If master is specified, create a subsession
|
||||
"""
|
||||
c = context.cnx.cursor()
|
||||
if not locked:
|
||||
#acquire a row lock on the user entry
|
||||
q = """SELECT id FROM users WHERE id=%(user_id)s"""
|
||||
c.execute(q,locals())
|
||||
|
||||
# generate a random key
|
||||
alnum = string.ascii_letters + string.digits
|
||||
|
|
|
|||
|
|
@ -20,8 +20,36 @@
|
|||
|
||||
import imp
|
||||
import koji
|
||||
import logging
|
||||
import sys
|
||||
import traceback
|
||||
|
||||
# set this up for use by the plugins
|
||||
# we want log output to go to Apache's error_log
|
||||
logger = logging.getLogger('koji.plugin')
|
||||
logger.addHandler(logging.StreamHandler(sys.stderr))
|
||||
logger.setLevel(logging.INFO)
|
||||
|
||||
# the available callback hooks and a list
|
||||
# of functions to be called for each event
|
||||
callbacks = {
|
||||
'prePackageListChange': [],
|
||||
'postPackageListChange': [],
|
||||
'preTaskStateChange': [],
|
||||
'postTaskStateChange': [],
|
||||
'preBuildStateChange': [],
|
||||
'postBuildStateChange': [],
|
||||
'preImport': [],
|
||||
'postImport': [],
|
||||
'preTag': [],
|
||||
'postTag': [],
|
||||
'preUntag': [],
|
||||
'postUntag': [],
|
||||
'preRepoInit': [],
|
||||
'postRepoInit': [],
|
||||
'preRepoDone': [],
|
||||
'postRepoDone': []
|
||||
}
|
||||
|
||||
class PluginTracker(object):
|
||||
|
||||
|
|
@ -41,11 +69,11 @@ class PluginTracker(object):
|
|||
#(no '.' -- it causes problems)
|
||||
mod_name = self.prefix + name
|
||||
if sys.modules.has_key(mod_name) and not reload:
|
||||
raise koji.GenericError, 'module name conflict: %s' % mod_name
|
||||
raise koji.PluginError, 'module name conflict: %s' % mod_name
|
||||
if path is None:
|
||||
path = self.searchpath
|
||||
if path is None:
|
||||
raise koji.GenericError, "empty module search path"
|
||||
raise koji.PluginError, "empty module search path"
|
||||
file, pathname, description = imp.find_module(name, self.pathlist(path))
|
||||
try:
|
||||
plugin = imp.load_module(mod_name, file, pathname, description)
|
||||
|
|
@ -101,3 +129,44 @@ def export_in(module, alias=None):
|
|||
setattr(f, 'export_alias', alias)
|
||||
return f
|
||||
return dec
|
||||
|
||||
def callback(*cbtypes):
|
||||
"""A decorator that indicates a function is a callback.
|
||||
cbtypes is a list of callback types to register for. Valid
|
||||
callback types are listed in the plugin module.
|
||||
|
||||
Intended to be used by plugins.
|
||||
"""
|
||||
def dec(f):
|
||||
setattr(f, 'callbacks', cbtypes)
|
||||
return f
|
||||
return dec
|
||||
|
||||
def ignore_error(f):
|
||||
"""a decorator that marks a callback as ok to fail
|
||||
|
||||
intended to be used by plugins
|
||||
"""
|
||||
setattr(f, 'failure_is_an_option', True)
|
||||
return f
|
||||
|
||||
def register_callback(cbtype, func):
|
||||
if not cbtype in callbacks:
|
||||
raise koji.PluginError, '"%s" is not a valid callback type' % cbtype
|
||||
if not callable(func):
|
||||
raise koji.PluginError, '%s is not callable' % getattr(func, '__name__', 'function')
|
||||
callbacks[cbtype].append(func)
|
||||
|
||||
def run_callbacks(cbtype, *args, **kws):
|
||||
if not cbtype in callbacks:
|
||||
raise koji.PluginError, '"%s" is not a valid callback type' % cbtype
|
||||
for func in callbacks[cbtype]:
|
||||
try:
|
||||
func(cbtype, *args, **kws)
|
||||
except:
|
||||
tb = ''.join(traceback.format_exception(*sys.exc_info()))
|
||||
msg = 'Error running %s callback from %s: %s' % (cbtype, func.__module__, tb)
|
||||
if getattr(func, 'failure_is_an_option', False):
|
||||
logging.getLogger('koji.plugin').warn('%s: %s' % (msg, tb))
|
||||
else:
|
||||
raise koji.CallbackError, msg
|
||||
|
|
|
|||
20
plugins/Makefile
Normal file
20
plugins/Makefile
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
PYTHON=python
|
||||
PLUGINDIR = /usr/lib/koji-hub-plugins
|
||||
FILES = $(wildcard *.py)
|
||||
|
||||
_default:
|
||||
@echo "nothing to make. try make install"
|
||||
|
||||
clean:
|
||||
rm -f *.o *.so *.pyc *.pyo *~
|
||||
|
||||
install:
|
||||
@if [ "$(DESTDIR)" = "" ]; then \
|
||||
echo " "; \
|
||||
echo "ERROR: A destdir is required"; \
|
||||
exit 1; \
|
||||
fi
|
||||
|
||||
mkdir -p $(DESTDIR)/$(PLUGINDIR)
|
||||
install -p -m 644 $(FILES) $(DESTDIR)/$(PLUGINDIR)
|
||||
$(PYTHON) -c "import compileall; compileall.compile_dir('$(DESTDIR)/$(PLUGINDIR)', 1, '$(PLUGINDIR)', 1)"
|
||||
15
plugins/echo.py
Normal file
15
plugins/echo.py
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
# Example Koji callback
|
||||
# Copyright (c) 2009 Red Hat, Inc.
|
||||
# This callback simply logs all of its args using the logging module
|
||||
#
|
||||
# Authors:
|
||||
# Mike Bonnet <mikeb@redhat.com>
|
||||
|
||||
from koji.plugin import callbacks, callback, ignore_error
|
||||
import logging
|
||||
|
||||
@callback(*callbacks.keys())
|
||||
@ignore_error
|
||||
def echo(cbtype, *args, **kws):
|
||||
logging.getLogger('koji.plugin.echo').info('Called the %s callback, args: %s; kws: %s',
|
||||
cbtype, str(args), str(kws))
|
||||
|
|
@ -756,7 +756,8 @@ class BuildTracker(object):
|
|||
for dep_id in build.deps:
|
||||
dep = self.scanBuild(dep_id, from_build=build, depth=depth+1, tag=tag)
|
||||
if dep.name in self.ignorelist:
|
||||
break
|
||||
# we are not done dep solving yet. but we dont want this dep in our buildroot
|
||||
continue
|
||||
else:
|
||||
if dep.substitute:
|
||||
dep2 = self.getSubstitute(dep.substitute)
|
||||
|
|
|
|||
|
|
@ -502,9 +502,18 @@ def taskinfo(req, taskID):
|
|||
|
||||
builds = server.listBuilds(taskID=task['id'])
|
||||
if builds:
|
||||
values['taskBuild'] = builds[0]
|
||||
taskBuild = builds[0]
|
||||
else:
|
||||
values['taskBuild'] = None
|
||||
taskBuild = None
|
||||
values['taskBuild'] = taskBuild
|
||||
|
||||
values['estCompletion'] = None
|
||||
if taskBuild and taskBuild['state'] == koji.BUILD_STATES['BUILDING']:
|
||||
avgDuration = server.getAverageBuildDuration(taskBuild['package_id'])
|
||||
if avgDuration != None:
|
||||
avgDelta = datetime.timedelta(seconds=avgDuration)
|
||||
startTime = datetime.datetime.fromtimestamp(taskBuild['creation_ts'])
|
||||
values['estCompletion'] = startTime + avgDelta
|
||||
|
||||
buildroots = server.listBuildroots(taskID=task['id'])
|
||||
values['buildroots'] = buildroots
|
||||
|
|
|
|||
|
|
@ -225,9 +225,17 @@ $value
|
|||
<tr>
|
||||
<th>Created</th><td>$util.formatTimeLong($task.create_time)</td>
|
||||
</tr>
|
||||
#if $task.state == $koji.TASK_STATES.OPEN
|
||||
#if $estCompletion
|
||||
<tr>
|
||||
<th>Est. Completion</th><td>$util.formatTimeLong($estCompletion)</td>
|
||||
</tr>
|
||||
#end if
|
||||
#elif $task.completion_time
|
||||
<tr>
|
||||
<th>Completed</th><td>$util.formatTimeLong($task.completion_time)</td>
|
||||
</tr>
|
||||
#end if
|
||||
<tr>
|
||||
<th>Owner</th>
|
||||
<td>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue