basic kiwi support

This commit is contained in:
Tomas Kopecek 2021-09-22 10:04:06 +02:00
parent 1bbb55e672
commit d31306e2f6
3 changed files with 500 additions and 0 deletions

387
plugins/builder/kiwi.py Normal file
View file

@ -0,0 +1,387 @@
import glob
import pickle
import os
import xml.dom.minidom
from fnmatch import fnmatch
import koji
from koji.util import joinpath, to_list
from koji.tasks import ServerExit
from __main__ import BaseBuildTask, BuildImageTask, BuildRoot, SCM
class KiwiBuildTask(BuildImageTask):
Methods = ['kiwiBuild']
_taskWeight = 4.0
def get_nvr(self, desc_path):
# TODO: update release in desc
kiwi_files = glob.glob('%s/*.kiwi' % desc_path)
if len(kiwi_files) != 1:
raise koji.GenericError("Repo must contain only one .kiwi file.")
cfg = kiwi_files[0]
newxml = xml.dom.minidom.parse(cfg)
image = newxml.getElementsByTagName('image')[0]
name = image.getAttribute('name')
version = None
release = None
for preferences in image.getElementsByTagName('preferences'):
try:
version = preferences.getElementsByTagName('version')[0].childNodes[0].data
except Exception:
pass
try:
release = preferences.getElementsByTagName('release')[0].childNodes[0].data
except Exception:
release = None
if not version:
raise koji.BuildError("Description file doesn't contain preferences/version")
return name, version, release
def handler(self, target, arches, desc_url, desc_path, opts=None):
target_info = self.session.getBuildTarget(target, strict=True)
build_tag = target_info['build_tag']
repo_info = self.getRepo(build_tag)
# check requested arches against build tag
buildconfig = self.session.getBuildConfig(build_tag)
if not buildconfig['arches']:
raise koji.BuildError("No arches for tag %(name)s [%(id)s]" % buildconfig)
tag_archlist = [koji.canonArch(a) for a in buildconfig['arches'].split()]
if arches:
for arch in arches:
if koji.canonArch(arch) not in tag_archlist:
raise koji.BuildError("Invalid arch for build tag: %s" % arch)
else:
arches = tag_archlist
if not opts:
opts = {}
if not opts.get('scratch'):
opts['scratch'] = False
if not opts.get('optional_arches'):
opts['optional_arches'] = []
self.opts = opts
# get configuration
scm = SCM(desc_url)
scm.assert_allowed(allowed=self.options.allowed_scms,
session=self.session,
by_config=self.options.allowed_scms_use_config,
by_policy=self.options.allowed_scms_use_policy,
policy_data={
'user_id': self.taskinfo['owner'],
'channel': self.session.getChannel(self.taskinfo['channel_id'],
strict=True)['name'],
'scratch': opts['scratch'],
})
logfile = os.path.join(self.workdir, 'checkout.log')
self.run_callbacks('preSCMCheckout', scminfo=scm.get_info(),
build_tag=build_tag, scratch=opts['scratch'])
scmdir = self.workdir
koji.ensuredir(scmdir)
scmsrcdir = scm.checkout(scmdir, self.session,
self.getUploadDir(), logfile)
self.run_callbacks("postSCMCheckout",
scminfo=scm.get_info(),
build_tag=build_tag,
scratch=opts['scratch'],
srcdir=scmsrcdir)
path = os.path.join(scmsrcdir, desc_path)
name, version, release = self.get_nvr(path)
bld_info = {}
if not opts['scratch']:
bld_info = self.initImageBuild(name, version, release, target_info, opts)
release = bld_info['release']
elif not release:
release = self.session.getNextRelease({'name': name, 'version': version})
try:
subtasks = {}
canfail = []
self.logger.debug("Spawning jobs for image arches: %r" % (arches))
for arch in arches:
subtasks[arch] = self.session.host.subtask(
method='createKiwiImage',
arglist=[name, version, release, arch,
target_info, build_tag, repo_info,
desc_url, desc_path, opts],
label=arch, parent=self.id, arch=arch)
if arch in self.opts['optional_arches']:
canfail.append(subtasks[arch])
self.logger.debug("Got image subtasks: %r" % (subtasks))
self.logger.debug("Waiting on image subtasks (%s can fail)..." % canfail)
results = self.wait(to_list(subtasks.values()), all=True,
failany=True, canfail=canfail)
# if everything failed, fail even if all subtasks are in canfail
self.logger.debug('subtask results: %r', results)
all_failed = True
for result in results.values():
if not isinstance(result, dict) or 'faultCode' not in result:
all_failed = False
break
if all_failed:
raise koji.GenericError("all subtasks failed")
# determine ignored arch failures
ignored_arches = set()
for arch in arches:
if arch in self.opts['optional_arches']:
task_id = subtasks[arch]
result = results[task_id]
if isinstance(result, dict) and 'faultCode' in result:
ignored_arches.add(arch)
self.logger.debug('Image Results for hub: %s' % results)
results = dict([(str(k), v) for k, v in results.items()])
if opts['scratch']:
self.session.host.moveImageBuildToScratch(self.id, results)
else:
self.session.host.completeImageBuild(self.id, bld_info['id'], results)
except (SystemExit, ServerExit, KeyboardInterrupt):
# we do not trap these
raise
except Exception:
if not opts['scratch']:
if bld_info:
self.session.host.failBuild(self.id, bld_info['id'])
raise
# tag it
if not opts['scratch'] and not opts.get('skip_tag'):
tag_task_id = self.session.host.subtask(method='tagBuild',
arglist=[target_info['dest_tag'],
bld_info['id'], False, None, True],
label='tag', parent=self.id, arch='noarch')
self.wait(tag_task_id)
# report results
report = ''
if opts['scratch']:
respath = ', '.join(
[os.path.join(koji.pathinfo.work(),
koji.pathinfo.taskrelpath(tid)) for tid in subtasks.values()])
report += 'Scratch '
else:
respath = koji.pathinfo.imagebuild(bld_info)
report += 'image build results in: %s' % respath
return report
class KiwiCreateImageTask(BaseBuildTask):
Methods = ['createKiwiImage']
_taskWeight = 2.0
def prepareDescription(self, desc_path, release, repos):
# TODO: update release in desc
kiwi_files = glob.glob('%s/*.kiwi' % desc_path)
if len(kiwi_files) != 1:
raise koji.GenericError("Repo must contain only one .kiwi file.")
cfg = kiwi_files[0]
newxml = xml.dom.minidom.parse(cfg)
image = newxml.getElementsByTagName('image')[0]
# remove old repos
for old_repo in image.getElementsByTagName('repository'):
image.removeChild(old_repo)
# add new ones
for repo in sorted(set(repos)):
repo_node = newxml.createElement('repository')
repo_node.setAttribute('type', 'rpm-md')
source = newxml.createElement('source')
source.setAttribute('path', repo)
repo_node.appendChild(source)
image.appendChild(repo_node)
# TODO: release is part of version (major.minor.release)
# preferences = image.getElementsByTagName('preferences')[0]
# try:
# preferences.getElementsByTagName('release')[0].childNodes[0].data = release
# except Exception:
# rel_node = newxml.createElement('release')
# rel_node.data = release
# preferences.appendChild(rel_node)
types = []
for pref in image.getElementsByTagName('preferences'):
for type in pref.getElementsByTagName('type'):
# TODO: if type.getAttribute('primary') == 'true':
types.append(type.getAttribute('image'))
# write file back
with open(cfg, 'wt') as f:
f.write(newxml.toprettyxml())
return cfg, types
def getImagePackagesFromCache(self, cachepath):
"""
Read RPM header information from the yum cache available in the
given path. Returns a list of dictionaries for each RPM included.
"""
found = False
hdrlist = {}
fields = ['name', 'version', 'release', 'epoch', 'arch',
'buildtime', 'sigmd5']
for root, dirs, files in os.walk(cachepath):
for f in files:
if fnmatch(f, '*.rpm'):
pkgfile = os.path.join(root, f)
hdr = koji.get_header_fields(pkgfile, fields)
hdr['size'] = os.path.getsize(pkgfile)
hdr['payloadhash'] = koji.hex_string(hdr['sigmd5'])
del hdr['sigmd5']
hdrlist[os.path.basename(pkgfile)] = hdr
found = True
if not found:
raise koji.LiveCDError('No repos found in yum cache!')
return list(hdrlist.values())
def getImagePackages(self, result):
"""Proper handler for getting rpminfo from result list,
it need result list to contain payloadhash, etc. to work correctly"""
hdrlist = []
for line in open(result, 'rt'):
line = line.strip()
name, epoch, version, release, arch, disturl, license = line.split('|')
try:
# "(none)" for None epochs
epoch = int(epoch)
except ValueError:
epoch = None
hdrlist.append({
'name': name,
'epoch': epoch,
'version': version,
'release': release,
'arch': arch,
'payloadhash': '',
'size': 0,
'buildtime': 0,
})
return hdrlist
def handler(self, name, version, release, arch,
target_info, build_tag, repo_info,
desc_url, desc_path, opts=None):
self.opts = opts
build_tag = target_info['build_tag']
broot = BuildRoot(self.session, self.options,
tag=build_tag,
arch=arch,
task_id=self.id,
repo_id=repo_info['id'],
install_group='kiwi',
setup_dns=True,
bind_opts={'dirs': {'/dev': '/dev', }})
broot.workdir = self.workdir
# create the mock chroot
self.logger.debug("Initializing kiwi buildroot")
broot.init()
self.logger.debug("Kiwi buildroot ready: " + broot.rootdir())
# get configuration
scm = SCM(desc_url)
scm.assert_allowed(allowed=self.options.allowed_scms,
session=self.session,
by_config=self.options.allowed_scms_use_config,
by_policy=self.options.allowed_scms_use_policy,
policy_data={
'user_id': self.taskinfo['owner'],
'channel': self.session.getChannel(self.taskinfo['channel_id'],
strict=True)['name'],
'scratch': self.opts.get('scratch')
})
logfile = os.path.join(self.workdir, 'checkout-%s.log' % arch)
self.run_callbacks('preSCMCheckout', scminfo=scm.get_info(),
build_tag=build_tag, scratch=self.opts.get('scratch'))
scmdir = broot.tmpdir()
koji.ensuredir(scmdir)
scmsrcdir = scm.checkout(scmdir, self.session,
self.getUploadDir(), logfile)
self.run_callbacks("postSCMCheckout",
scminfo=scm.get_info(),
build_tag=build_tag,
scratch=self.opts.get('scratch'),
srcdir=scmsrcdir)
# user repos
repos = self.opts.get('repos', [])
# buildroot repo
path_info = koji.PathInfo(topdir=self.options.topurl)
repopath = path_info.repo(repo_info['id'], target_info['build_tag_name'])
baseurl = '%s/%s' % (repopath, arch)
self.logger.debug('BASEURL: %s' % baseurl)
repos.append(baseurl)
path = os.path.join(scmsrcdir, desc_path)
desc, types = self.prepareDescription(path, release, repos)
self.uploadFile(desc)
cmd = ['kiwi-ng']
if self.opts.get('profile'):
cmd.extend(['--profile', self.opts['profile']])
target_dir = '/builddir/result/image'
cmd.extend([
'system', 'build',
'--description', os.path.join(os.path.basename(scmsrcdir), desc_path),
'--target-dir', target_dir,
])
rv = broot.mock(['--cwd', broot.tmpdir(within=True), '--chroot', '--'] + cmd)
if rv:
raise koji.GenericError("Kiwi failed")
result = pickle.load(open(joinpath(broot.rootdir(), target_dir[1:], 'kiwi.result'), 'rb'))
imgdata = {
'arch': arch,
'task_id': self.id,
'logs': [
os.path.basename(desc)
],
'name': name,
'version': version,
'release': release,
'rpmlist': [],
'files': [],
}
# TODO: upload detailed log?
# build/image-root.log
# os.path.join(broot.tmpdir(), target_dir[1:], "build/image-root.log")
# for type in types:
# img_file = '%s.%s-%s.%s' % (name, version, arch, type)
# self.uploadFile(os.path.join(broot.rootdir()), remoteName=img_file)
# imgdata['files'].append(img_file)
fpath = os.path.join(broot.rootdir(),
result.result_files['disk_format_image'].filename[1:])
img_file = os.path.basename(fpath)
self.uploadFile(fpath, remoteName=os.path.basename(img_file))
imgdata['files'].append(img_file)
if not self.opts.get('scratch'):
if False:
# should be used after kiwi update
fpath = os.path.join(broot.rootdir(),
result.result_files['image_packages'].filename[1:])
hdrlist = self.getImagePackages(fpath)
else:
cachepath = os.path.join(broot.rootdir(), 'var/cache/kiwi/dnf')
hdrlist = self.getImagePackagesFromCache(cachepath)
broot.markExternalRPMs(hdrlist)
imgdata['rpmlist'] = hdrlist
broot.expire()
self.logger.error("Uploading image data: %s", imgdata)
return imgdata

75
plugins/cli/kiwi.py Normal file
View file

@ -0,0 +1,75 @@
import os
from optparse import OptionParser
from koji import canonArch
from koji.plugin import export_cli
from koji_cli.lib import (
_,
_running_in_bg,
activate_session,
watch_tasks,
)
@export_cli
def handle_kiwi_build(goptions, session, args):
"[build] Run a command in a buildroot"
usage = _("usage: %prog kiwi-build [options] "
"<target> <description_scm> <description_path>")
usage += _("\n(Specify the --help global option for a list of other help options)")
parser = OptionParser(usage=usage)
parser.add_option("--scratch", action="store_true", default=False,
help=_("Perform a scratch build"))
parser.add_option("--repo", action="append",
help=_("Specify a repo that will override the repo used to install "
"RPMs in the image. May be used multiple times. The "
"build tag repo associated with the target is the default."))
parser.add_option("--noprogress", action="store_true",
help=_("Do not display progress of the upload"))
parser.add_option("--kiwi-profile", action="store", default=None,
help=_("Select profile from description file"))
parser.add_option("--can-fail", action="store", dest="optional_arches",
metavar="ARCH1,ARCH2,...", default="",
help=_("List of archs which are not blocking for build "
"(separated by commas."))
parser.add_option("--arch", action="append", dest="arches", default=[],
help=_("Limit arches to this subset"))
parser.add_option("--nowait", action="store_false", dest="wait", default=True)
parser.add_option("--wait", action="store_true",
help=_("Wait on the image creation, even if running in the background"))
(options, args) = parser.parse_args(args)
if len(args) != 3:
parser.error(_("Incorrect number of arguments"))
assert False # pragma: no cover
target, scm, path = args
activate_session(session, goptions)
kwargs = {
'scratch': options.scratch,
'optional_arches': [canonArch(arch)
for arch in options.optional_arches.split(',')
if arch],
'profile': options.kiwi_profile,
}
arches = []
if options.arches:
arches = [canonArch(arch) for arch in options.arches]
task_id = session.kiwiBuild(
target=target,
arches=arches,
desc_url=scm,
desc_path=path,
**kwargs)
if not goptions.quiet:
print("Created task: %d" % task_id)
print("Task info: %s/taskinfo?taskID=%s" % (goptions.weburl, task_id))
if options.wait or (options.wait is None and not _running_in_bg()):
session.logout()
return watch_tasks(session, [task_id], quiet=goptions.quiet,
poll_interval=goptions.poll_interval, topurl=goptions.topurl)

38
plugins/hub/kiwi.py Normal file
View file

@ -0,0 +1,38 @@
import koji
import koji.tasks
import kojihub
from koji.context import context
from koji.plugin import export
koji.tasks.LEGACY_SIGNATURES['kiwiBuild'] = [
[['target', 'arches', 'desc_url', 'desc_path', 'opts'],
None, None, (None,)]]
koji.tasks.LEGACY_SIGNATURES['createKiwiImage'] = [
[['name', 'version', 'release', 'arch',
'target_info', 'build_tag', 'repo_info', 'desc_url', 'opts'],
None, None, (None,)]]
@export
def kiwiBuild(target, arches, desc_url, desc_path, optional_arches=None, profile=None,
scratch=False, priority=None):
context.session.assertPerm('image')
taskOpts = {
'channel': 'image',
}
if priority:
if priority < 0:
if not context.session.hasPerm('admin'):
raise koji.ActionNotAllowed(
'only admins may create high-priority tasks')
taskOpts['priority'] = koji.PRIO_DEFAULT + priority
opts = {
'optional_arches': optional_arches,
'profile': profile,
'scratch': scratch,
}
return kojihub.make_task('kiwiBuild',
[target, arches, desc_url, desc_path, opts],
**taskOpts)