This patch refactors logic for creating a temporary mount point, mounting an image, running arbitrary code on it, unmounting the image and removing the mount point. It immediately uses it in the buildinstall phase. Similar mounting is present in product_img phase as well, but due to different usage pattern it's not changed yet. Signed-off-by: Lubomír Sedlář <lsedlar@redhat.com>
413 lines
11 KiB
Python
413 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/>.
|
|
|
|
|
|
import os
|
|
import sys
|
|
import pipes
|
|
from fnmatch import fnmatch
|
|
import contextlib
|
|
|
|
from kobo.shortcuts import force_list, relative_path, run
|
|
from pungi import util
|
|
|
|
|
|
# HACK: define cmp in python3
|
|
if sys.version_info[0] == 3:
|
|
def cmp(a, b):
|
|
return (a > b) - (a < b)
|
|
|
|
|
|
def get_boot_options(arch, createfrom, efi=True):
|
|
"""Checks to see what we need as the -b option for mkisofs"""
|
|
|
|
if arch in ("arm", "armhfp"):
|
|
result = []
|
|
return result
|
|
|
|
if arch in ("aarch64", ):
|
|
result = [
|
|
'-eltorito-alt-boot',
|
|
'-e', 'images/efiboot.img',
|
|
'-no-emul-boot',
|
|
]
|
|
return result
|
|
|
|
if arch in ("i386", "i686", "x86_64"):
|
|
result = [
|
|
'-b', 'isolinux/isolinux.bin',
|
|
'-c', 'isolinux/boot.cat',
|
|
'-no-emul-boot',
|
|
'-boot-load-size', '4',
|
|
'-boot-info-table',
|
|
]
|
|
|
|
# EFI args
|
|
if arch == "x86_64":
|
|
result.extend([
|
|
'-eltorito-alt-boot',
|
|
'-e', 'images/efiboot.img',
|
|
'-no-emul-boot',
|
|
])
|
|
return result
|
|
|
|
if arch == "ia64":
|
|
result = [
|
|
'-b', 'images/boot.img',
|
|
'-no-emul-boot',
|
|
]
|
|
return result
|
|
|
|
if arch in ("ppc", "ppc64", "ppc64le"):
|
|
result = [
|
|
'-part',
|
|
'-hfs',
|
|
'-r',
|
|
'-l',
|
|
'-sysid', 'PPC',
|
|
'-no-desktop',
|
|
'-allow-multidot',
|
|
'-chrp-boot',
|
|
"-map", os.path.join(createfrom, 'mapping'), # -map %s/ppc/mapping
|
|
'-hfs-bless', "/ppc/mac", # must be the last
|
|
]
|
|
return result
|
|
|
|
if arch == "sparc":
|
|
result = [
|
|
'-G', '/boot/isofs.b',
|
|
'-B', '...',
|
|
'-s', '/boot/silo.conf',
|
|
'-sparc-label', '"sparc"',
|
|
]
|
|
return result
|
|
|
|
if arch in ("s390", "s390x"):
|
|
result = [
|
|
# "-no-emul-boot",
|
|
# "-b", "images/cdboot.img",
|
|
# "-c", "boot.cat",
|
|
]
|
|
return result
|
|
|
|
raise ValueError("Unknown arch: %s" % arch)
|
|
|
|
|
|
def _truncate_volid(volid):
|
|
if len(volid) > 32:
|
|
volid = volid.replace("-", "")
|
|
|
|
if len(volid) > 32:
|
|
volid = volid.replace(" ", "")
|
|
|
|
if len(volid) > 32:
|
|
volid = volid.replace("Supplementary", "Supp")
|
|
|
|
if len(volid) > 32:
|
|
raise ValueError("Volume ID must be less than 32 character: %s" % volid)
|
|
|
|
return volid
|
|
|
|
|
|
def get_mkisofs_cmd(iso, paths, appid=None, volid=None, volset=None, exclude=None, verbose=False, boot_args=None, input_charset="utf-8", graft_points=None):
|
|
# following options are always enabled
|
|
untranslated_filenames = True
|
|
translation_table = True
|
|
joliet = True
|
|
joliet_long = True
|
|
rock = True
|
|
|
|
cmd = ["/usr/bin/genisoimage"]
|
|
if appid:
|
|
cmd.extend(["-appid", appid])
|
|
|
|
if untranslated_filenames:
|
|
cmd.append("-untranslated-filenames")
|
|
|
|
if volid:
|
|
cmd.extend(["-volid", _truncate_volid(volid)])
|
|
|
|
if joliet:
|
|
cmd.append("-J")
|
|
|
|
if joliet_long:
|
|
cmd.append("-joliet-long")
|
|
|
|
if volset:
|
|
cmd.extend(["-volset", volset])
|
|
|
|
if rock:
|
|
cmd.append("-rational-rock")
|
|
|
|
if verbose:
|
|
cmd.append("-verbose")
|
|
|
|
if translation_table:
|
|
cmd.append("-translation-table")
|
|
|
|
if input_charset:
|
|
cmd.extend(["-input-charset", input_charset])
|
|
|
|
if exclude:
|
|
for i in force_list(exclude):
|
|
cmd.extend(["-x", i])
|
|
|
|
if boot_args:
|
|
cmd.extend(boot_args)
|
|
|
|
cmd.extend(["-o", iso])
|
|
|
|
if graft_points:
|
|
cmd.append("-graft-points")
|
|
cmd.extend(["-path-list", graft_points])
|
|
else:
|
|
# we're either using graft points or file lists, not both
|
|
cmd.extend(force_list(paths))
|
|
|
|
return cmd
|
|
|
|
|
|
def get_implantisomd5_cmd(iso_path, supported=False):
|
|
cmd = ["/usr/bin/implantisomd5"]
|
|
if supported:
|
|
cmd.append("--supported-iso")
|
|
cmd.append(iso_path)
|
|
return cmd
|
|
|
|
|
|
def get_checkisomd5_cmd(iso_path, just_print=False):
|
|
cmd = ["/usr/bin/checkisomd5"]
|
|
if just_print:
|
|
cmd.append("--md5sumonly")
|
|
cmd.append(iso_path)
|
|
return cmd
|
|
|
|
|
|
def get_implanted_md5(iso_path, logger=None):
|
|
cmd = get_checkisomd5_cmd(iso_path, just_print=True)
|
|
retcode, output = run(cmd)
|
|
line = output.splitlines()[0]
|
|
md5 = line.rsplit(":")[-1].strip()
|
|
if len(md5) != 32:
|
|
# We have seen cases where the command finished successfully, but
|
|
# returned garbage value. We need to handle it, otherwise there would
|
|
# be a crash once we try to write image metadata.
|
|
# This only logs information about the problem and leaves the hash
|
|
# empty, which is valid from productmd point of view.
|
|
if logger:
|
|
logger.critical('Implanted MD5 in %s is not valid: %r', iso_path, md5)
|
|
logger.critical('Ran command %r; exit code %r; output %r', cmd, retcode, output)
|
|
return None
|
|
return md5
|
|
|
|
|
|
def get_isohybrid_cmd(iso_path, arch):
|
|
# isohybrid is in syslinux which is x86 only
|
|
cmd = ["/usr/bin/isohybrid"]
|
|
# uefi is only supported on x86_64
|
|
if arch == "x86_64":
|
|
cmd.append("--uefi")
|
|
cmd.append(iso_path)
|
|
return cmd
|
|
|
|
|
|
def get_manifest_cmd(iso_name):
|
|
return "isoinfo -R -f -i %s | grep -v '/TRANS.TBL$' | sort >> %s.manifest" % (pipes.quote(iso_name), pipes.quote(iso_name))
|
|
|
|
|
|
def get_volume_id(path):
|
|
cmd = ["isoinfo", "-d", "-i", path]
|
|
retcode, output = run(cmd)
|
|
|
|
for line in output.splitlines():
|
|
line = line.strip()
|
|
if line.startswith("Volume id:"):
|
|
return line[11:].strip()
|
|
|
|
raise RuntimeError("Could not read Volume ID")
|
|
|
|
|
|
def get_graft_points(paths, exclusive_paths=None, exclude=None):
|
|
# path priority in ascending order (1st = lowest prio)
|
|
# paths merge according to priority
|
|
# exclusive paths override whole dirs
|
|
|
|
result = {}
|
|
exclude = exclude or []
|
|
exclusive_paths = exclusive_paths or []
|
|
|
|
for i in paths:
|
|
if isinstance(i, dict):
|
|
tree = i
|
|
else:
|
|
tree = _scan_tree(i)
|
|
result = _merge_trees(result, tree)
|
|
|
|
for i in exclusive_paths:
|
|
tree = _scan_tree(i)
|
|
result = _merge_trees(result, tree, exclusive=True)
|
|
|
|
# TODO: exclude
|
|
return result
|
|
|
|
def _paths_from_list(root, paths):
|
|
root = os.path.abspath(root).rstrip("/") + "/"
|
|
result = {}
|
|
for i in paths:
|
|
i = os.path.normpath(os.path.join(root, i))
|
|
key = i[len(root):]
|
|
result[key] = i
|
|
return result
|
|
|
|
|
|
def _scan_tree(path):
|
|
path = os.path.abspath(path)
|
|
result = {}
|
|
for root, dirs, files in os.walk(path):
|
|
for f in files:
|
|
abspath = os.path.join(root, f)
|
|
relpath = relative_path(abspath, path.rstrip("/") + "/")
|
|
result[relpath] = abspath
|
|
|
|
# include empty dirs
|
|
if root != path:
|
|
abspath = os.path.join(root, "")
|
|
relpath = relative_path(abspath, path.rstrip("/") + "/")
|
|
result[relpath] = abspath
|
|
|
|
return result
|
|
|
|
|
|
def _merge_trees(tree1, tree2, exclusive=False):
|
|
# tree2 has higher priority
|
|
result = tree2.copy()
|
|
all_dirs = set([os.path.dirname(i).rstrip("/") for i in result if os.path.dirname(i) != ""])
|
|
|
|
for i in tree1:
|
|
dn = os.path.dirname(i)
|
|
if exclusive:
|
|
match = False
|
|
for a in all_dirs:
|
|
if dn == a or dn.startswith("%s/" % a):
|
|
match = True
|
|
break
|
|
if match:
|
|
continue
|
|
|
|
if i in result:
|
|
continue
|
|
|
|
result[i] = tree1[i]
|
|
return result
|
|
|
|
|
|
def write_graft_points(file_name, h, exclude=None):
|
|
exclude = exclude or []
|
|
result = {}
|
|
seen_dirs = set()
|
|
for i in sorted(h, reverse=True):
|
|
dn = os.path.dirname(i)
|
|
|
|
if not i.endswith("/"):
|
|
result[i] = h[i]
|
|
seen_dirs.add(dn)
|
|
continue
|
|
|
|
found = False
|
|
for j in seen_dirs:
|
|
if j.startswith(dn):
|
|
found = True
|
|
break
|
|
if not found:
|
|
result[i] = h[i]
|
|
seen_dirs.add(dn)
|
|
|
|
f = open(file_name, "w")
|
|
for i in sorted(result, cmp=cmp_graft_points):
|
|
# make sure all files required for boot come first,
|
|
# otherwise there may be problems with booting (large LBA address, etc.)
|
|
found = False
|
|
for excl in exclude:
|
|
if fnmatch(i, excl):
|
|
found = True
|
|
break
|
|
if found:
|
|
continue
|
|
f.write("%s=%s\n" % (i, h[i]))
|
|
f.close()
|
|
|
|
|
|
def _is_rpm(path):
|
|
if path.endswith(".rpm"):
|
|
return True
|
|
return False
|
|
|
|
|
|
def _is_image(path):
|
|
if path.startswith("images/"):
|
|
return True
|
|
if path.startswith("isolinux/"):
|
|
return True
|
|
if path.startswith("EFI/"):
|
|
return True
|
|
if path.startswith("etc/"):
|
|
return True
|
|
if path.startswith("ppc/"):
|
|
return True
|
|
if path.endswith(".img"):
|
|
return True
|
|
if path.endswith(".ins"):
|
|
return True
|
|
return False
|
|
|
|
|
|
def cmp_graft_points(x, y):
|
|
x_is_rpm = _is_rpm(x)
|
|
y_is_rpm = _is_rpm(y)
|
|
x_is_image = _is_image(x)
|
|
y_is_image = _is_image(y)
|
|
|
|
if x_is_rpm and y_is_rpm:
|
|
return cmp(x, y)
|
|
if x_is_rpm:
|
|
return 1
|
|
if y_is_rpm:
|
|
return -1
|
|
|
|
if x_is_image and y_is_image:
|
|
return cmp(x, y)
|
|
if x_is_image:
|
|
return -1
|
|
if y_is_image:
|
|
return 1
|
|
|
|
return cmp(x, y)
|
|
|
|
|
|
@contextlib.contextmanager
|
|
def mount(image, logger=None):
|
|
"""Mount an image and make sure it's unmounted.
|
|
|
|
The yielded path will only be valid in the with block and is removed once
|
|
the image is unmounted.
|
|
"""
|
|
# use guestmount to mount the image, which doesn't require root privileges
|
|
# LIBGUESTFS_BACKEND=direct: running qemu directly without libvirt
|
|
with util.temp_dir(prefix='iso-mount-') as mount_dir:
|
|
run(["LIBGUESTFS_BACKEND=direct", "guestmount", "-a", image, "-m", "/dev/sda", mount_dir])
|
|
try:
|
|
yield mount_dir
|
|
finally:
|
|
util.fusermount(mount_dir, logger=logger)
|