2152e7ea26
Relates: https://pagure.io/pungi/issue/803 Signed-off-by: Ondrej Nosek <onosek@redhat.com>
276 lines
11 KiB
Python
276 lines
11 KiB
Python
# -*- coding: utf-8 -*-
|
|
|
|
|
|
# This program is free software; you can redistribute it and/or modify
|
|
# it under the terms of the GNU General Public License as published by
|
|
# the Free Software Foundation; version 2 of the License.
|
|
#
|
|
# This program 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 Library General Public License for more details.
|
|
#
|
|
# You should have received a copy of the GNU General Public License
|
|
# along with this program; if not, see <https://gnu.org/licenses/>.
|
|
|
|
|
|
"""
|
|
Expected product.img paths
|
|
==========================
|
|
|
|
RHEL 6
|
|
------
|
|
installclasses/$variant.py
|
|
locale/$lang/LC_MESSAGES/comps.mo
|
|
|
|
RHEL 7
|
|
------
|
|
run/install/product/installclasses/$variant.py
|
|
run/install/product/locale/$lang/LC_MESSAGES/comps.mo
|
|
|
|
Compatibility symlinks
|
|
----------------------
|
|
installclasses -> run/install/product/installclasses
|
|
locale -> run/install/product/locale
|
|
run/install/product/pyanaconda/installclasses -> ../installclasses
|
|
"""
|
|
|
|
|
|
import os
|
|
import fnmatch
|
|
import shutil
|
|
from six.moves import shlex_quote
|
|
|
|
from kobo.shortcuts import run
|
|
|
|
from pungi.arch import split_name_arch
|
|
from pungi.util import makedirs, pkg_is_rpm
|
|
from pungi.phases.base import PhaseBase
|
|
from pungi.wrappers import iso
|
|
from pungi.wrappers.scm import get_file_from_scm, get_dir_from_scm
|
|
|
|
|
|
class ProductimgPhase(PhaseBase):
|
|
"""PRODUCTIMG"""
|
|
name = "productimg"
|
|
|
|
def __init__(self, compose, pkgset_phase):
|
|
PhaseBase.__init__(self, compose)
|
|
# pkgset_phase provides package_sets and path_prefix
|
|
self.pkgset_phase = pkgset_phase
|
|
|
|
def skip(self):
|
|
if PhaseBase.skip(self):
|
|
return True
|
|
if not self.compose.conf["productimg"]:
|
|
msg = "Config option 'productimg' not set. Skipping creating product images."
|
|
self.compose.log_debug(msg)
|
|
return True
|
|
if not self.compose.conf["bootable"]:
|
|
msg = "Not a bootable product. Skipping creating product images."
|
|
self.compose.log_debug(msg)
|
|
return True
|
|
return False
|
|
|
|
def run(self):
|
|
# create PRODUCT.IMG
|
|
for variant in self.compose.get_variants():
|
|
if variant.type != "variant" or variant.is_empty:
|
|
continue
|
|
create_product_img(self.compose, "global", variant)
|
|
|
|
# copy PRODUCT.IMG
|
|
for arch in self.compose.get_arches():
|
|
for variant in self.compose.get_variants(arch=arch):
|
|
if variant.type != "variant" or variant.is_empty:
|
|
continue
|
|
image = self.compose.paths.work.product_img(variant)
|
|
os_tree = self.compose.paths.compose.os_tree(arch, variant)
|
|
target_dir = os.path.join(os_tree, "images")
|
|
target_path = os.path.join(target_dir, "product.img")
|
|
if not os.path.isfile(target_path):
|
|
makedirs(target_dir)
|
|
shutil.copy2(image, target_path)
|
|
|
|
for arch in self.compose.get_arches():
|
|
for variant in self.compose.get_variants(arch=arch):
|
|
if variant.type != "variant" or variant.is_empty:
|
|
continue
|
|
rebuild_boot_iso(self.compose, arch, variant, self.pkgset_phase.package_sets)
|
|
|
|
|
|
def create_product_img(compose, arch, variant):
|
|
# product.img is noarch (at least on rhel6 and rhel7)
|
|
arch = "global"
|
|
|
|
msg = "Creating product.img (arch: %s, variant: %s)" % (arch, variant)
|
|
image = compose.paths.work.product_img(variant)
|
|
if os.path.exists(image):
|
|
compose.log_warning("[SKIP ] %s" % msg)
|
|
return
|
|
|
|
compose.log_info("[BEGIN] %s" % msg)
|
|
|
|
product_tmp = compose.mkdtemp(prefix="product_img_")
|
|
install_class = compose.conf["productimg_install_class"].copy()
|
|
install_class["file"] = install_class["file"] % {"variant_id": variant.id.lower()}
|
|
install_dir = os.path.join(product_tmp, "installclasses")
|
|
makedirs(install_dir)
|
|
get_file_from_scm(install_class, target_path=install_dir, logger=None)
|
|
|
|
po_files = compose.conf["productimg_po_files"]
|
|
po_tmp = compose.mkdtemp(prefix="pofiles_")
|
|
get_dir_from_scm(po_files, po_tmp, logger=compose._logger)
|
|
for po_file in os.listdir(po_tmp):
|
|
if not po_file.endswith(".po"):
|
|
continue
|
|
lang = po_file[:-3]
|
|
target_dir = os.path.join(product_tmp, "locale", lang, "LC_MESSAGES")
|
|
makedirs(target_dir)
|
|
run(["msgfmt", "--output-file", os.path.join(target_dir, "comps.mo"), os.path.join(po_tmp, po_file)])
|
|
|
|
shutil.rmtree(po_tmp)
|
|
|
|
ret, __ = run(["which", "guestmount"], can_fail=True)
|
|
guestmount_available = not bool(ret) # return code 0 means that guestmount is available
|
|
|
|
mount_tmp = compose.mkdtemp(prefix="product_img_mount_")
|
|
cmds = [
|
|
# allocate image
|
|
"dd if=/dev/zero of=%s bs=1k count=5760" % shlex_quote(image),
|
|
# create file system
|
|
"mke2fs -F %s" % shlex_quote(image),
|
|
# use guestmount to mount the image, which doesn't require root privileges
|
|
# LIBGUESTFS_BACKEND=direct: running qemu directly without libvirt
|
|
"LIBGUESTFS_BACKEND=direct guestmount -a %s -m /dev/sda %s" % (shlex_quote(image), shlex_quote(mount_tmp)) if guestmount_available
|
|
else "mount -o loop %s %s" % (shlex_quote(image), shlex_quote(mount_tmp)),
|
|
"mkdir -p %s/run/install/product" % shlex_quote(mount_tmp),
|
|
"cp -rp %s/* %s/run/install/product/" % (shlex_quote(product_tmp), shlex_quote(mount_tmp)),
|
|
"mkdir -p %s/run/install/product/pyanaconda" % shlex_quote(mount_tmp),
|
|
# compat symlink: installclasses -> run/install/product/installclasses
|
|
"ln -s run/install/product/installclasses %s" % shlex_quote(mount_tmp),
|
|
# compat symlink: locale -> run/install/product/locale
|
|
"ln -s run/install/product/locale %s" % shlex_quote(mount_tmp),
|
|
# compat symlink: run/install/product/pyanaconda/installclasses -> ../installclasses
|
|
"ln -s ../installclasses %s/run/install/product/pyanaconda/installclasses" % shlex_quote(mount_tmp),
|
|
"fusermount -u %s" % shlex_quote(mount_tmp) if guestmount_available
|
|
else "umount %s" % shlex_quote(mount_tmp),
|
|
# tweak last mount path written in the image
|
|
"tune2fs -M /run/install/product %s" % shlex_quote(image),
|
|
]
|
|
run(" && ".join(cmds))
|
|
shutil.rmtree(mount_tmp)
|
|
|
|
shutil.rmtree(product_tmp)
|
|
|
|
compose.log_info("[DONE ] %s" % msg)
|
|
|
|
|
|
def rebuild_boot_iso(compose, arch, variant, package_sets):
|
|
os_tree = compose.paths.compose.os_tree(arch, variant)
|
|
buildinstall_dir = compose.paths.work.buildinstall_dir(arch)
|
|
boot_iso = os.path.join(os_tree, "images", "boot.iso")
|
|
product_img = compose.paths.work.product_img(variant)
|
|
buildinstall_boot_iso = os.path.join(buildinstall_dir, "images", "boot.iso")
|
|
buildinstall_method = compose.conf["buildinstall_method"]
|
|
log_file = compose.paths.log.log_file(arch, "rebuild_boot_iso-%s.%s" % (variant, arch))
|
|
|
|
msg = "Rebuilding boot.iso (arch: %s, variant: %s)" % (arch, variant)
|
|
|
|
if not os.path.isfile(boot_iso):
|
|
# nothing to do
|
|
compose.log_warning("[SKIP ] %s" % msg)
|
|
return
|
|
|
|
compose.log_info("[BEGIN] %s" % msg)
|
|
|
|
# read the original volume id
|
|
volume_id = iso.get_volume_id(boot_iso)
|
|
|
|
# remove the original boot.iso (created during buildinstall) from the os dir
|
|
os.remove(boot_iso)
|
|
|
|
tmp_dir = compose.mkdtemp(prefix="boot_iso_")
|
|
mount_dir = compose.mkdtemp(prefix="boot_iso_mount_")
|
|
|
|
cmd = "mount -o loop %s %s" % (shlex_quote(buildinstall_boot_iso), shlex_quote(mount_dir))
|
|
run(cmd, logfile=log_file, show_cmd=True)
|
|
|
|
images_dir = os.path.join(tmp_dir, "images")
|
|
os.makedirs(images_dir)
|
|
shutil.copy2(product_img, os.path.join(images_dir, "product.img"))
|
|
|
|
if os.path.isfile(os.path.join(mount_dir, "isolinux", "isolinux.bin")):
|
|
os.makedirs(os.path.join(tmp_dir, "isolinux"))
|
|
shutil.copy2(os.path.join(mount_dir, "isolinux", "isolinux.bin"), os.path.join(tmp_dir, "isolinux"))
|
|
|
|
graft_points = iso.get_graft_points([mount_dir, tmp_dir])
|
|
graft_points_path = os.path.join(compose.paths.work.topdir(arch=arch), "boot-%s.%s.iso-graft-points" % (variant, arch))
|
|
iso.write_graft_points(graft_points_path, graft_points, exclude=["*/TRANS.TBL", "*/boot.cat"])
|
|
|
|
mkisofs_kwargs = {}
|
|
boot_files = None
|
|
if buildinstall_method == "lorax":
|
|
# TODO: $arch instead of ppc
|
|
mkisofs_kwargs["boot_args"] = iso.get_boot_options(arch, "/usr/share/lorax/config_files/ppc")
|
|
elif buildinstall_method == "buildinstall":
|
|
boot_files = explode_anaconda(compose, arch, variant, package_sets)
|
|
mkisofs_kwargs["boot_args"] = iso.get_boot_options(arch, boot_files)
|
|
|
|
# ppc(64) doesn't seem to support utf-8
|
|
if arch in ("ppc", "ppc64"):
|
|
mkisofs_kwargs["input_charset"] = None
|
|
|
|
mkisofs_cmd = iso.get_mkisofs_cmd(boot_iso, None, volid=volume_id, exclude=["./lost+found"], graft_points=graft_points_path, **mkisofs_kwargs)
|
|
run(mkisofs_cmd, logfile=log_file, show_cmd=True)
|
|
|
|
cmd = "umount %s" % shlex_quote(mount_dir)
|
|
run(cmd, logfile=log_file, show_cmd=True)
|
|
|
|
if arch == "x86_64":
|
|
isohybrid_cmd = "isohybrid --uefi %s" % shlex_quote(boot_iso)
|
|
run(isohybrid_cmd, logfile=log_file, show_cmd=True)
|
|
elif arch == "i386":
|
|
isohybrid_cmd = "isohybrid %s" % shlex_quote(boot_iso)
|
|
run(isohybrid_cmd, logfile=log_file, show_cmd=True)
|
|
|
|
# implant MD5SUM to iso
|
|
isomd5sum_cmd = iso.get_implantisomd5_cmd(boot_iso, compose.supported)
|
|
isomd5sum_cmd = " ".join([shlex_quote(i) for i in isomd5sum_cmd])
|
|
run(isomd5sum_cmd, logfile=log_file, show_cmd=True)
|
|
|
|
if boot_files:
|
|
shutil.rmtree(boot_files)
|
|
shutil.rmtree(tmp_dir)
|
|
shutil.rmtree(mount_dir)
|
|
|
|
compose.log_info("[DONE ] %s" % msg)
|
|
|
|
|
|
def explode_anaconda(compose, arch, variant, package_sets):
|
|
tmp_dir = compose.mkdtemp(prefix="anaconda_")
|
|
scm_dict = {
|
|
"scm": "rpm",
|
|
"repo": "anaconda.%s" % arch,
|
|
"file": [
|
|
"/usr/lib/anaconda-runtime/boot/*",
|
|
]
|
|
}
|
|
# if scm is "rpm" and repo contains a package name, find the package(s) in package set
|
|
if scm_dict["scm"] == "rpm" and not (scm_dict["repo"].startswith("/") or "://" in scm_dict["repo"]):
|
|
rpms = []
|
|
for pkgset_file in package_sets[arch]:
|
|
pkg_obj = package_sets[arch][pkgset_file]
|
|
if not pkg_is_rpm(pkg_obj):
|
|
continue
|
|
pkg_name, pkg_arch = split_name_arch(scm_dict["repo"])
|
|
if fnmatch.fnmatch(pkg_obj.name, pkg_name) and (pkg_arch is None or pkg_arch == pkg_obj.arch):
|
|
compose.log_critical("%s %s %s" % (pkg_obj.name, pkg_name, pkg_arch))
|
|
rpms.append(pkg_obj.file_path)
|
|
scm_dict["repo"] = rpms
|
|
|
|
if not rpms:
|
|
return None
|
|
get_file_from_scm(scm_dict, tmp_dir, logger=compose._logger)
|
|
return tmp_dir
|