debian-koji/koji/plugin.py
2021-02-04 10:44:21 +01:00

229 lines
6.7 KiB
Python

# koji plugin module
# Copyright (c) 2008-2014 Red Hat, Inc.
#
# 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:
# Mike McLean <mikem@redhat.com>
# Mike Bonnet <mikeb@redhat.com>
from __future__ import absolute_import
import imp
import logging
import sys
import traceback
import six
import koji
from koji.util import encode_datetime_recurse
# the available callback hooks and a list
# of functions to be called for each event
callbacks = {
# hub
'prePackageListChange': [],
'postPackageListChange': [],
'preTaskStateChange': [],
'postTaskStateChange': [],
'preBuildStateChange': [],
'postBuildStateChange': [],
'preImport': [],
'postImport': [],
'preRPMSign': [],
'postRPMSign': [],
'preTag': [],
'postTag': [],
'preUntag': [],
'postUntag': [],
'preRepoInit': [],
'postRepoInit': [],
'preRepoDone': [],
'postRepoDone': [],
'preCommit': [],
'postCommit': [],
# builder
'preSCMCheckout': [],
'postSCMCheckout': [],
'postCreateDistRepo': [],
'postCreateRepo': [],
}
class PluginTracker(object):
def __init__(self, path=None, prefix='_koji_plugin__'):
self.searchpath = path
# prefix should not have a '.' in it, this can cause problems.
self.prefix = prefix
self.plugins = {}
def load(self, name, path=None, reload=False):
if name in self.plugins and not reload:
return self.plugins[name]
mod_name = name
if self.prefix:
# mod_name determines how the module is named in sys.modules
# Using a prefix helps prevent overlap with other modules
# (no '.' -- it causes problems)
mod_name = self.prefix + name
if mod_name in sys.modules and not reload:
raise koji.PluginError('module name conflict: %s' % mod_name)
if path is None:
path = self.searchpath
if path is None:
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)
except Exception:
msg = 'Loading plugin %s failed' % name
logging.getLogger('koji.plugin').error(msg)
raise
finally:
file.close()
self.plugins[name] = plugin
return plugin
def get(self, name):
return self.plugins.get(name)
def pathlist(self, path):
if isinstance(path, six.string_types):
return [path]
else:
return path
# some decorators used by plugins
def export(f):
"""a decorator that marks a function as exported
intended to be used by plugins
the HandlerRegistry will export the function under its own name
"""
setattr(f, 'exported', True)
return f
def export_cli(f):
"""a decorator that marks a function as exported for CLI
intended to be used by plugins
the HandlerRegistry will export the function under its own name
"""
setattr(f, 'exported_cli', True)
return f
def export_as(alias):
"""returns a decorator that marks a function as exported and gives it an alias
indended to be used by plugins
"""
def dec(f):
setattr(f, 'exported', True)
setattr(f, 'export_alias', alias)
return f
return dec
def export_in(module, alias=None):
"""returns a decorator that marks a function as exported with a module prepended
optionally, can also alias the function within the module
indended to be used by plugins
"""
def dec(f):
if alias is None:
local_alias = "%s.%s" % (module, f.__name__)
else:
local_alias = "%s.%s" % (module, alias)
setattr(f, 'exported', True)
setattr(f, 'export_module', module)
setattr(f, 'export_alias', local_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 convert_datetime(f):
"""Indicate that callback needs to receive datetime objects as strings"""
setattr(f, 'convert_datetime', True)
return f
def register_callback(cbtype, func):
if cbtype not 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 cbtype not in callbacks:
raise koji.PluginError('"%s" is not a valid callback type' % cbtype)
cache = {}
for func in callbacks[cbtype]:
cb_args, cb_kwargs = _fix_cb_args(func, args, kws, cache)
try:
func(cbtype, *cb_args, **cb_kwargs)
except Exception:
msg = 'Error running %s callback from %s' % (cbtype, func.__module__)
if getattr(func, 'failure_is_an_option', False):
logging.getLogger('koji.plugin').warning(msg, exc_info=True)
else:
tb = ''.join(traceback.format_exception(*sys.exc_info()))
raise koji.CallbackError('%s:\n%s' % (msg, tb))
def _fix_cb_args(func, args, kwargs, cache):
if getattr(func, 'convert_datetime', False):
if id(args) in cache:
args = cache[id(args)]
else:
val = encode_datetime_recurse(args)
cache[id(args)] = val
args = val
if id(kwargs) in cache:
kwargs = cache[id(kwargs)]
else:
val = encode_datetime_recurse(kwargs)
cache[id(kwargs)] = val
kwargs = val
return args, kwargs