2015-09-01 08:03:34 +00:00
|
|
|
# -*- coding: utf-8 -*-
|
|
|
|
|
2016-01-05 09:20:00 +00:00
|
|
|
import copy
|
2015-09-01 08:03:34 +00:00
|
|
|
import os
|
|
|
|
import time
|
2016-01-11 08:03:04 +00:00
|
|
|
from kobo import shortcuts
|
2015-09-01 08:03:34 +00:00
|
|
|
|
2017-08-18 07:33:51 +00:00
|
|
|
from pungi.util import makedirs, get_mtime, get_file_size, failable
|
2017-04-07 13:33:43 +00:00
|
|
|
from pungi.util import translate_path, get_repo_urls, version_generator
|
2016-04-14 12:23:42 +00:00
|
|
|
from pungi.phases import base
|
2015-09-01 08:03:34 +00:00
|
|
|
from pungi.linker import Linker
|
|
|
|
from pungi.wrappers.kojiwrapper import KojiWrapper
|
|
|
|
from kobo.threads import ThreadPool, WorkerThread
|
2017-10-30 12:23:49 +00:00
|
|
|
from kobo.shortcuts import force_list
|
2015-09-01 08:03:34 +00:00
|
|
|
from productmd.images import Image
|
|
|
|
|
2015-11-20 09:53:55 +00:00
|
|
|
|
2017-10-30 12:23:49 +00:00
|
|
|
# This is a mapping from formats to file extensions. The format is what koji
|
|
|
|
# image-build command expects as argument, and the extension is what the file
|
|
|
|
# name will be ending with. The extensions are used to filter out which task
|
|
|
|
# results will be pulled into the compose.
|
|
|
|
EXTENSIONS = {
|
|
|
|
'docker': 'tar.gz',
|
|
|
|
'liveimg-squashfs': 'liveimg.squashfs',
|
|
|
|
'qcow': 'qcow',
|
|
|
|
'qcow2': 'qcow2',
|
|
|
|
'raw': 'raw',
|
|
|
|
'raw-xz': 'raw.xz',
|
|
|
|
'rhevm-ova': 'rhevm.ova',
|
|
|
|
'tar-gz': 'tar.gz',
|
|
|
|
'vagrant-hyperv': 'vagrant-hyperv.box',
|
|
|
|
'vagrant-libvirt': 'vagrant-libvirt.box',
|
|
|
|
'vagrant-virtualbox': 'vagrant-virtualbox.box',
|
|
|
|
'vagrant-vmware-fusion': 'vagrant-vmware-fusion.box',
|
|
|
|
'vdi': 'vdi',
|
|
|
|
'vmdk': 'vdmk',
|
|
|
|
'vpc': 'vhd',
|
|
|
|
'vsphere-ova': 'vsphere.ova',
|
|
|
|
}
|
|
|
|
|
|
|
|
|
2016-11-18 20:33:03 +00:00
|
|
|
class ImageBuildPhase(base.PhaseLoggerMixin, base.ImageConfigMixin, base.ConfigGuardedPhase):
|
2015-09-01 08:03:34 +00:00
|
|
|
"""class for wrapping up koji image-build"""
|
|
|
|
name = "image_build"
|
|
|
|
|
|
|
|
def __init__(self, compose):
|
2016-04-14 09:24:05 +00:00
|
|
|
super(ImageBuildPhase, self).__init__(compose)
|
2016-11-18 20:33:03 +00:00
|
|
|
self.pool = ThreadPool(logger=self.logger)
|
2015-09-01 08:03:34 +00:00
|
|
|
|
2016-01-08 12:37:09 +00:00
|
|
|
def _get_install_tree(self, image_conf, variant):
|
|
|
|
"""
|
|
|
|
Get a path to os tree for a variant specified in `install_tree_from` or
|
|
|
|
current variant. If the config is set, it will be removed from the
|
|
|
|
dict.
|
|
|
|
"""
|
2016-11-24 13:53:30 +00:00
|
|
|
if variant.type != 'variant':
|
|
|
|
# Buildinstall only runs for top-level variants. Nested variants
|
|
|
|
# need to re-use install tree from parent.
|
|
|
|
variant = variant.parent
|
|
|
|
|
2016-01-08 12:37:09 +00:00
|
|
|
install_tree_from = image_conf.pop('install_tree_from', variant.uid)
|
2016-05-23 13:23:12 +00:00
|
|
|
if '://' in install_tree_from:
|
|
|
|
return install_tree_from
|
2016-11-09 09:20:55 +00:00
|
|
|
install_tree_source = self.compose.all_variants.get(install_tree_from)
|
2016-01-08 12:37:09 +00:00
|
|
|
if not install_tree_source:
|
|
|
|
raise RuntimeError(
|
|
|
|
'There is no variant %s to get install tree from when building image for %s.'
|
|
|
|
% (install_tree_from, variant.uid))
|
|
|
|
return translate_path(
|
|
|
|
self.compose,
|
2016-01-19 07:24:02 +00:00
|
|
|
self.compose.paths.compose.os_tree('$arch', install_tree_source, create_dir=False)
|
2016-01-08 12:37:09 +00:00
|
|
|
)
|
|
|
|
|
2016-01-11 08:03:04 +00:00
|
|
|
def _get_repo(self, image_conf, variant):
|
|
|
|
"""
|
|
|
|
Get a comma separated list of repos. First included are those
|
2017-03-27 21:53:08 +00:00
|
|
|
explicitly listed in config, followed by by repo for current variant
|
|
|
|
if it's not included in the list already.
|
2016-01-11 08:03:04 +00:00
|
|
|
"""
|
2017-03-27 21:53:08 +00:00
|
|
|
repos = shortcuts.force_list(image_conf.get('repo', []))
|
2016-01-11 08:03:04 +00:00
|
|
|
|
2017-03-27 21:53:08 +00:00
|
|
|
if not variant.is_empty and variant.uid not in repos:
|
|
|
|
repos.append(variant.uid)
|
2016-01-11 08:03:04 +00:00
|
|
|
|
2017-03-27 21:53:08 +00:00
|
|
|
return ",".join(get_repo_urls(self.compose, repos, arch='$arch'))
|
2016-01-11 08:03:04 +00:00
|
|
|
|
2016-01-11 08:06:13 +00:00
|
|
|
def _get_arches(self, image_conf, arches):
|
2016-02-10 16:24:45 +00:00
|
|
|
if 'arches' in image_conf['image-build']:
|
|
|
|
arches = set(image_conf['image-build'].get('arches', [])) & arches
|
2017-01-24 07:36:33 +00:00
|
|
|
return sorted(arches)
|
2016-01-11 08:06:13 +00:00
|
|
|
|
2016-01-21 13:30:44 +00:00
|
|
|
def _set_release(self, image_conf):
|
|
|
|
"""If release is set explicitly to None, replace it with date and respin."""
|
2017-04-07 13:33:43 +00:00
|
|
|
if 'release' in image_conf:
|
|
|
|
image_conf['release'] = (version_generator(self.compose, image_conf['release']) or
|
|
|
|
self.compose.image_release)
|
2016-01-21 13:30:44 +00:00
|
|
|
|
2015-09-01 08:03:34 +00:00
|
|
|
def run(self):
|
2016-01-05 08:27:20 +00:00
|
|
|
for variant in self.compose.get_variants():
|
|
|
|
arches = set([x for x in variant.arches if x != 'src'])
|
|
|
|
|
2017-08-18 07:33:51 +00:00
|
|
|
for image_conf in self.get_config_block(variant):
|
2016-01-05 08:27:20 +00:00
|
|
|
# We will modify the data, so we need to make a copy to
|
|
|
|
# prevent problems in next iteration where the original
|
|
|
|
# value is needed.
|
|
|
|
image_conf = copy.deepcopy(image_conf)
|
|
|
|
|
|
|
|
# image_conf is passed to get_image_build_cmd as dict
|
|
|
|
|
2016-02-10 16:24:45 +00:00
|
|
|
image_conf["image-build"]['arches'] = self._get_arches(image_conf, arches)
|
|
|
|
if not image_conf["image-build"]['arches']:
|
2016-01-05 08:27:20 +00:00
|
|
|
continue
|
|
|
|
|
2016-01-11 08:06:13 +00:00
|
|
|
# Replace possible ambiguous ref name with explicit hash.
|
2016-04-14 12:23:42 +00:00
|
|
|
ksurl = self.get_ksurl(image_conf['image-build'])
|
|
|
|
if ksurl:
|
|
|
|
image_conf["image-build"]['ksurl'] = ksurl
|
2016-01-11 08:06:13 +00:00
|
|
|
|
2016-02-10 16:24:45 +00:00
|
|
|
image_conf["image-build"]["variant"] = variant
|
2016-01-08 12:37:09 +00:00
|
|
|
|
2016-02-11 12:22:03 +00:00
|
|
|
image_conf["image-build"]["install_tree"] = self._get_install_tree(image_conf['image-build'], variant)
|
2016-01-08 12:37:09 +00:00
|
|
|
|
2016-04-14 12:23:42 +00:00
|
|
|
release = self.get_release(image_conf['image-build'])
|
|
|
|
if release:
|
|
|
|
image_conf['image-build']['release'] = release
|
|
|
|
|
2016-08-30 07:51:36 +00:00
|
|
|
image_conf['image-build']['version'] = self.get_version(image_conf['image-build'])
|
2016-04-14 12:23:42 +00:00
|
|
|
image_conf['image-build']['target'] = self.get_config(image_conf['image-build'], 'target')
|
2016-01-21 13:30:44 +00:00
|
|
|
|
2017-10-30 12:23:49 +00:00
|
|
|
# Pungi config can either contain old [(format, suffix)], or
|
|
|
|
# just list of formats, or a single format.
|
|
|
|
formats = []
|
|
|
|
for format in force_list(image_conf["image-build"]["format"]):
|
|
|
|
formats.append(format[0] if isinstance(format, tuple) else format)
|
|
|
|
image_conf["image-build"]["format"] = formats
|
2016-02-11 12:22:03 +00:00
|
|
|
image_conf["image-build"]['repo'] = self._get_repo(image_conf['image-build'], variant)
|
2016-01-05 08:27:20 +00:00
|
|
|
|
2016-11-28 14:28:00 +00:00
|
|
|
can_fail = image_conf['image-build'].pop('failable', [])
|
|
|
|
if can_fail == ['*']:
|
2017-01-24 07:36:33 +00:00
|
|
|
can_fail = image_conf['image-build']['arches']
|
2016-11-28 14:28:00 +00:00
|
|
|
if can_fail:
|
2017-01-24 07:36:33 +00:00
|
|
|
image_conf['image-build']['can_fail'] = sorted(can_fail)
|
2016-11-28 14:28:00 +00:00
|
|
|
|
2016-01-05 08:27:20 +00:00
|
|
|
cmd = {
|
|
|
|
"image_conf": image_conf,
|
|
|
|
"conf_file": self.compose.paths.work.image_build_conf(
|
2016-02-10 16:24:45 +00:00
|
|
|
image_conf["image-build"]['variant'],
|
|
|
|
image_name=image_conf["image-build"]['name'],
|
2017-10-30 12:23:49 +00:00
|
|
|
image_type='-'.join(formats),
|
image-build: add arch name(s) in image config file name
Pungi write image config file with name of <format>-<name>.cfg, if there
are two or more image configs present for different arches under the same
variant and with same format & name, the config file can be overwritten,
and result in invalid image conf file.
Example:
image_build = {
'^Server$': [
{
'image-build': {
'format': [('qcow2', 'qcow2'),],
'name': 'fedora-guest-image',
'target': 'guest-fedora-26-image',
'version': '26',
'ksurl': "git://git.example.com/ks.git?fedora#HEAD",
'kickstart': "fedora-26-kvm.ks",
'ksversion': 'f26',
'distro': 'fedora-26',
'disk-size': '10',
'arches': ['x86_64'],
'repo': ["http://example.com/linux/fedora/26/Everything/x86_64/os", ]
}
},
{
'image-build': {
'format': [('qcow2', 'qcow2'),],
'name': 'fedora-guest-image',
'target': 'guest-fedora-26-image',
'version': '26',
'ksurl': "git://git.example.com/ks.git?fedora#HEAD",
'kickstart': "fedora-26-kvm.ks",
'ksversion': 'f26',
'distro': 'fedora-26',
'disk-size': '10',
'arches': ['ppc64le'],
}
},
],
}
In this case, config file "qcow2_guest-fedora-26-image.cfg" will be
created for both x86_64 and ppc64le under the same variant dir, and
there is a high chance it will be over-written while Pungi creating the
koji task. We can add arch name(s) in config filename to avoid that.
Signed-off-by: Qixiang Wan <qwan@redhat.com>
2017-08-31 08:10:52 +00:00
|
|
|
arches=image_conf["image-build"]['arches'],
|
2016-01-05 08:27:20 +00:00
|
|
|
),
|
|
|
|
"image_dir": self.compose.paths.compose.image_dir(variant),
|
|
|
|
"relative_image_dir": self.compose.paths.compose.image_dir(
|
2016-01-19 07:24:02 +00:00
|
|
|
variant, relative=True
|
2016-01-05 08:27:20 +00:00
|
|
|
),
|
2016-08-22 14:08:25 +00:00
|
|
|
"link_type": self.compose.conf["link_type"],
|
2016-02-11 12:22:03 +00:00
|
|
|
"scratch": image_conf['image-build'].pop('scratch', False),
|
2016-01-05 08:27:20 +00:00
|
|
|
}
|
|
|
|
self.pool.add(CreateImageBuildThread(self.pool))
|
|
|
|
self.pool.queue_put((self.compose, cmd))
|
|
|
|
|
2015-09-01 08:03:34 +00:00
|
|
|
self.pool.start()
|
|
|
|
|
|
|
|
|
|
|
|
class CreateImageBuildThread(WorkerThread):
|
|
|
|
def fail(self, compose, cmd):
|
2016-11-18 20:33:03 +00:00
|
|
|
self.pool.log_error("CreateImageBuild failed.")
|
2015-09-01 08:03:34 +00:00
|
|
|
|
|
|
|
def process(self, item, num):
|
|
|
|
compose, cmd = item
|
2016-03-17 08:21:35 +00:00
|
|
|
variant = cmd["image_conf"]["image-build"]["variant"]
|
|
|
|
subvariant = cmd["image_conf"]["image-build"].get("subvariant", variant.uid)
|
2016-11-28 14:28:00 +00:00
|
|
|
self.failable_arches = cmd["image_conf"]['image-build'].get('can_fail', '')
|
|
|
|
self.can_fail = self.failable_arches == cmd['image_conf']['image-build']['arches']
|
2016-11-18 20:33:03 +00:00
|
|
|
with failable(compose, self.can_fail, variant, '*', 'image-build', subvariant,
|
|
|
|
logger=self.pool._logger):
|
2016-03-17 08:21:35 +00:00
|
|
|
self.worker(num, compose, variant, subvariant, cmd)
|
2016-01-11 16:14:51 +00:00
|
|
|
|
2016-03-17 08:21:35 +00:00
|
|
|
def worker(self, num, compose, variant, subvariant, cmd):
|
2017-01-24 07:36:33 +00:00
|
|
|
arches = cmd["image_conf"]["image-build"]['arches']
|
2017-10-30 12:23:49 +00:00
|
|
|
formats = '-'.join(cmd['image_conf']['image-build']['format'])
|
2016-02-15 08:03:34 +00:00
|
|
|
dash_arches = '-'.join(arches)
|
2016-01-05 08:27:20 +00:00
|
|
|
log_file = compose.paths.log.log_file(
|
2016-02-15 08:03:34 +00:00
|
|
|
dash_arches,
|
2017-10-30 12:23:49 +00:00
|
|
|
"imagebuild-%s-%s-%s" % (variant.uid, subvariant, formats)
|
2016-01-05 08:27:20 +00:00
|
|
|
)
|
2017-10-30 12:23:49 +00:00
|
|
|
msg = ("Creating image (formats: %s, arches: %s, variant: %s, subvariant: %s)"
|
|
|
|
% (formats, dash_arches, variant, subvariant))
|
2015-09-01 08:03:34 +00:00
|
|
|
self.pool.log_info("[BEGIN] %s" % msg)
|
|
|
|
|
|
|
|
koji_wrapper = KojiWrapper(compose.conf["koji_profile"])
|
|
|
|
|
|
|
|
# writes conf file for koji image-build
|
2016-01-05 08:27:20 +00:00
|
|
|
self.pool.log_info("Writing image-build config for %s.%s into %s" % (
|
2016-03-17 08:21:35 +00:00
|
|
|
variant, dash_arches, cmd["conf_file"]))
|
2017-01-24 07:36:33 +00:00
|
|
|
|
2016-02-10 16:24:45 +00:00
|
|
|
koji_cmd = koji_wrapper.get_image_build_cmd(cmd["image_conf"],
|
2016-01-26 11:46:50 +00:00
|
|
|
conf_file_dest=cmd["conf_file"],
|
|
|
|
scratch=cmd['scratch'])
|
2015-09-01 08:03:34 +00:00
|
|
|
|
|
|
|
# avoid race conditions?
|
|
|
|
# Kerberos authentication failed: Permission denied in replay cache code (-1765328215)
|
|
|
|
time.sleep(num * 3)
|
2016-01-28 13:58:24 +00:00
|
|
|
output = koji_wrapper.run_blocking_cmd(koji_cmd, log_file=log_file)
|
2015-09-01 08:03:34 +00:00
|
|
|
self.pool.log_debug("build-image outputs: %s" % (output))
|
|
|
|
if output["retcode"] != 0:
|
|
|
|
self.fail(compose, cmd)
|
2016-03-17 08:21:35 +00:00
|
|
|
raise RuntimeError("ImageBuild task failed: %s. See %s for more details."
|
|
|
|
% (output["task_id"], log_file))
|
2015-09-01 08:03:34 +00:00
|
|
|
|
|
|
|
# copy image to images/
|
|
|
|
image_infos = []
|
|
|
|
|
2016-01-28 13:58:24 +00:00
|
|
|
paths = koji_wrapper.get_image_paths(output["task_id"])
|
2016-01-05 08:27:20 +00:00
|
|
|
|
2017-09-05 08:01:21 +00:00
|
|
|
for arch, paths in paths.items():
|
2016-01-05 08:27:20 +00:00
|
|
|
for path in paths:
|
2017-10-30 12:23:49 +00:00
|
|
|
for format in cmd['image_conf']['image-build']['format']:
|
|
|
|
suffix = EXTENSIONS[format]
|
2016-01-05 08:27:20 +00:00
|
|
|
if path.endswith(suffix):
|
|
|
|
image_infos.append({'path': path, 'suffix': suffix, 'type': format, 'arch': arch})
|
|
|
|
break
|
|
|
|
|
2015-09-01 08:03:34 +00:00
|
|
|
# The usecase here is that you can run koji image-build with multiple --format
|
|
|
|
# It's ok to do it serialized since we're talking about max 2 images per single
|
|
|
|
# image_build record
|
2016-11-18 20:33:03 +00:00
|
|
|
linker = Linker(logger=self.pool._logger)
|
2015-09-01 08:03:34 +00:00
|
|
|
for image_info in image_infos:
|
2016-01-05 08:27:20 +00:00
|
|
|
image_dir = cmd["image_dir"] % {"arch": image_info['arch']}
|
2016-01-19 07:24:02 +00:00
|
|
|
makedirs(image_dir)
|
2016-01-05 08:27:20 +00:00
|
|
|
relative_image_dir = cmd["relative_image_dir"] % {"arch": image_info['arch']}
|
|
|
|
|
2015-09-01 08:03:34 +00:00
|
|
|
# let's not change filename of koji outputs
|
2016-01-05 08:27:20 +00:00
|
|
|
image_dest = os.path.join(image_dir, os.path.basename(image_info['path']))
|
|
|
|
linker.link(image_info['path'], image_dest, link_type=cmd["link_type"])
|
2015-09-01 08:03:34 +00:00
|
|
|
|
|
|
|
# Update image manifest
|
|
|
|
img = Image(compose.im)
|
|
|
|
img.type = image_info['type']
|
|
|
|
img.format = image_info['suffix']
|
2016-01-05 08:27:20 +00:00
|
|
|
img.path = os.path.join(relative_image_dir, os.path.basename(image_dest))
|
2016-02-11 14:15:36 +00:00
|
|
|
img.mtime = get_mtime(image_dest)
|
|
|
|
img.size = get_file_size(image_dest)
|
2016-01-05 08:27:20 +00:00
|
|
|
img.arch = image_info['arch']
|
|
|
|
img.disc_number = 1 # We don't expect multiple disks
|
2015-09-01 08:03:34 +00:00
|
|
|
img.disc_count = 1
|
|
|
|
img.bootable = False
|
2016-03-17 08:21:35 +00:00
|
|
|
img.subvariant = subvariant
|
2016-06-24 07:44:40 +00:00
|
|
|
setattr(img, 'can_fail', self.can_fail)
|
2016-05-03 14:31:20 +00:00
|
|
|
setattr(img, 'deliverable', 'image-build')
|
2016-03-17 08:21:35 +00:00
|
|
|
compose.im.add(variant=variant.uid, arch=image_info['arch'], image=img)
|
2015-09-01 08:03:34 +00:00
|
|
|
|
2017-03-15 15:16:18 +00:00
|
|
|
self.pool.log_info("[DONE ] %s (task id: %s)" % (msg, output['task_id']))
|