import os import xml.dom.minidom from fnmatch import fnmatch import koji import koji.util from koji.tasks import ServerExit from __main__ import BaseBuildTask, BuildImageTask, BuildRoot, SCM class KiwiBuildTask(BuildImageTask): Methods = ['kiwiBuild'] _taskWeight = 4.0 def get_nvrp(self, cfg): try: newxml = xml.dom.minidom.parse(cfg) # nosec except Exception: raise koji.GenericError( f"Kiwi description {os.path.basename(cfg)} can't be parsed as XML.") try: image = newxml.getElementsByTagName('image')[0] except IndexError: raise koji.GenericError( f"Kiwi description {os.path.basename(cfg)} doesn't contain tag.") name = image.getAttribute('name') version = None for preferences in image.getElementsByTagName('preferences'): try: version = preferences.getElementsByTagName('version')[0].childNodes[0].data except Exception: pass profile = None try: for p in image.getElementsByTagName('profiles')[0].getElementsByTagName('profile'): if p.getAttribute('image') == 'true': profile = p.getAttribute('name') except IndexError: # missing profiles section pass if not version: raise koji.BuildError("Description file doesn't contain preferences/version") return name, version, profile 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'] = [] if not buildconfig['extra'].get('mock.new_chroot', True): opts['mount_dev'] = True 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, default_profile = self.get_nvrp(path) if opts.get('profile') or default_profile: # package name is a combination of name + profile # in case profiles are not used, let's use the standalone name name = "%s-%s" % (name, opts.get('profile', default_profile)) bld_info = {} if opts.get('version'): version = opts['version'] if opts.get('release'): release = opts['release'] else: release = self.session.getNextRelease({'name': name, 'version': version}) if not opts['scratch']: bld_info = self.initImageBuild(name, version, release, target_info, opts) release = bld_info['release'] 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(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 = {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, name, version, repos, repo_releasever, arch): # XML errors should have already been caught by parent task newxml = xml.dom.minidom.parse(desc_path) # nosec image = newxml.getElementsByTagName('image')[0] # apply includes - kiwi can include only top-level nodes, so we can simply # go through "include" elements and replace them with referred content (without # doing it recursively) for inc_node in image.getElementsByTagName('include'): path = inc_node.getAttribute('from') if path.startswith('this://'): path = koji.util.joinpath(os.path.dirname(desc_path), path[7:]) else: # we want to reject other protocols, e.g. file://, https:// # reachingoutside of repo raise koji.GenericError(f"Unhandled include protocol in include path: {path}.") inc = xml.dom.minidom.parse(path) # nosec # every included xml has image root element again try: for node in list(inc.getElementsByTagName('image')[0].childNodes): if node.nodeName != 'repository': image.appendChild(node) except IndexError: raise koji.GenericError("Included file needs to contain tag.") image.removeChild(inc_node) # remove remaining old repos for old_repo in image.getElementsByTagName('repository'): image.removeChild(old_repo) # add koji 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) image.setAttribute('name', name) preferences = image.getElementsByTagName('preferences')[0] # Handle version and release-version preferences.getElementsByTagName('version')[0].childNodes[0].data = version try: preferences.getElementsByTagName('release-version')[0].childNodes[0].data \ = repo_releasever except IndexError: releasever_node = newxml.createElement('release-version') text = newxml.createTextNode(repo_releasever) releasever_node.appendChild(text) preferences.appendChild(releasever_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 new file newcfg = os.path.splitext(desc_path)[0] + f'.{arch}.kiwi' with open(newcfg, 'wt') as f: s = newxml.toprettyxml() # toprettyxml adds too many whitespaces/newlines s = '\n'.join([x for x in s.splitlines() if x.strip()]) f.write(s) os.unlink(desc_path) return newcfg, 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('|') if epoch == '(none)': epoch = None else: epoch = int(epoch) 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'] bind_opts = {'dirs': {}} if self.opts.get('mount_dev'): bind_opts['dirs']['/dev'] = '/dev' broot = BuildRoot(self.session, self.options, tag=build_tag, arch=arch, task_id=self.id, repo_id=repo_info['id'], install_group='kiwi-build', setup_dns=True, bind_opts=bind_opts) 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', False) }) 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', False)) 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', False), srcdir=scmsrcdir) # user repos repos = self.opts.get('repos', []) if self.opts.get('use_buildroot_repo', False): 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) repo_releasever = self.opts.get('repo_releasever', version) base_path = os.path.dirname(desc_path) if opts.get('make_prep'): cmd = ['make', 'prep'] rv = broot.mock(['--cwd', os.path.join(broot.tmpdir(within=True), os.path.basename(scmsrcdir), base_path), '--chroot', '--'] + cmd) if rv: raise koji.GenericError("Preparation step failed") path = os.path.join(scmsrcdir, desc_path) desc, types = self.prepareDescription(path, name, version, repos, repo_releasever, arch) self.uploadFile(desc) target_dir = '/builddir/result/image' os.symlink( # symlink log to resultdir, so it is incrementally uploaded os.path.join(broot.rootdir(), f'tmp/image-root.{arch}.log'), os.path.join(broot.resultdir(), f'image-root.{arch}.log') ) cmd = ['kiwi-ng'] if self.opts.get('profile'): cmd.extend(['--profile', self.opts['profile']]) if self.opts.get('type'): cmd.extend(['--type', self.opts['type']]) cmd.extend([ '--kiwi-file', os.path.basename(desc), # global option for image/system commands '--debug', '--logfile', f'/tmp/image-root.{arch}.log', 'system', 'build', '--description', os.path.join(os.path.basename(scmsrcdir), base_path), '--target-dir', target_dir, ]) for typeattr in self.opts.get('type_attr', []): cmd.extend(['--set-type-attr', typeattr]) rv = broot.mock(['--cwd', broot.tmpdir(within=True), '--chroot', '--'] + cmd) if rv: raise koji.GenericError("Kiwi failed") # rename artifacts accordingly to release os.symlink( # symlink log to resultdir, so it is incrementally uploaded os.path.join(broot.rootdir(), f'/tmp/kiwi-result-bundle.{arch}.log'), os.path.join(broot.resultdir(), f'kiwi-result-bundle.{arch}.log') ) bundle_dir = '/builddir/result/bundle' cmd = ['kiwi-ng', '--debug', '--logfile', f'/tmp/kiwi-result-bundle.{arch}.log', 'result', 'bundle', '--target-dir', target_dir, '--bundle-dir', bundle_dir, '--id', release] if self.opts.get('result_bundle_name_format'): cmd.extend(['--bundle-format', self.opts['result_bundle_name_format']]) rv = broot.mock(['--cwd', broot.tmpdir(within=True), '--chroot', '--'] + cmd) if rv: raise koji.GenericError("Kiwi failed") imgdata = { 'arch': arch, 'task_id': self.id, 'logs': [ os.path.basename(desc), ], 'name': name, 'version': version, 'release': release, 'rpmlist': [], 'files': [], } bundle_path = os.path.join(broot.rootdir(), bundle_dir[1:]) for fname in os.listdir(bundle_path): self.uploadFile(os.path.join(bundle_path, fname), remoteName=fname) imgdata['files'].append(fname) if not self.opts.get('scratch'): if False: # should be used after kiwi update fpath = os.path.join( bundle_path, next(f for f in imgdata['files'] if f.endswith('.packages')), ) 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