#!/usr/bin/python3 # # Live Media Creator # # Copyright (C) 2011-2018 Red Hat, Inc. # # 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; either version 2 of the License, or # (at your option) any later version. # # 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 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 . # import logging log = logging.getLogger("livemedia-creator") import os import sys import tempfile import shutil import glob import selinux # Use pykickstart to calculate disk image size from pykickstart.parser import KickstartParser from pykickstart.version import makeVersion from pykickstart.constants import KS_SHUTDOWN # Use the Lorax treebuilder branch for iso creation from pylorax import setup_logging, find_templates, vernum from pylorax.cmdline import lmc_parser from pylorax.creator import make_image, make_squashfs, make_livecd, make_runtime, make_appliance, make_live_images from pylorax.creator import calculate_disk_size from pylorax.imgutils import PartitionMount from pylorax.imgutils import Mount from pylorax.imgutils import copytree from pylorax.installer import InstallError from pylorax.sysutils import joinpaths def default_image_name(compression, basename): """ Return a default image name with the correct suffix for the compression type. :param str compression: Compression type :param str basename: Base filename :returns: basename with compression suffix If the compression is unknown it defaults to xz """ SUFFIXES = {"xz": ".xz", "gzip": ".gz", "bzip2": ".bz2", "lzma": ".lzma"} return basename + SUFFIXES.get(compression, ".xz") def main(): parser = lmc_parser() opts = parser.parse_args() setup_logging(opts.logfile, log) log.debug( opts ) log.info("livemedia-creator v%s", vernum) # Find the lorax templates opts.lorax_templates = find_templates(opts.lorax_templates or "/usr/share/lorax") # Check for invalid combinations of options, print all the errors and exit. errors = [] if not opts.disk_image and not opts.fs_image and not opts.ks: errors.append("Image creation requires a kickstart file") if opts.ks and not os.path.exists(opts.ks[0]): errors.append("kickstart file (%s) is missing." % opts.ks[0]) if opts.make_iso and not os.path.exists(opts.lorax_templates): errors.append("The lorax templates directory (%s) doesn't " "exist." % opts.lorax_templates) if opts.result_dir and os.path.exists(opts.result_dir): errors.append("The results_dir (%s) should not exist, please delete or " "move its contents" % opts.result_dir) # Default to putting results under tmp if not opts.result_dir: opts.result_dir = opts.tmp if opts.iso and not os.path.exists(opts.iso): errors.append("The iso %s is missing." % opts.iso) if opts.disk_image and not os.path.exists(opts.disk_image): errors.append("The disk image %s is missing." % opts.disk_image) if opts.fs_image and not os.path.exists(opts.fs_image): errors.append("The filesystem image %s is missing." % opts.fs_image) is_install = not (opts.disk_image or opts.fs_image) if is_install and not opts.no_virt and not opts.iso: errors.append("virt install needs an install iso.") if opts.volid and len(opts.volid) > 32: errors.append("the volume id cannot be longer than 32 characters") if is_install and not opts.no_virt \ and not any(glob.glob("/usr/bin/qemu-system-*")): errors.append("qemu needs to be installed.") if is_install and opts.no_virt \ and not os.path.exists("/usr/sbin/anaconda"): errors.append("no-virt requires anaconda to be installed.") if is_install and opts.no_virt: if selinux.is_selinux_enabled() and selinux.security_getenforce(): errors.append("selinux must be disabled or in Permissive mode.") if opts.make_appliance and not opts.app_template: opts.app_template = joinpaths(opts.lorax_templates, "appliance/libvirt.tmpl") if opts.make_appliance and not os.path.exists(opts.app_template): errors.append("The appliance template (%s) doesn't " "exist" % opts.app_template) if opts.image_name and os.path.exists(joinpaths(opts.result_dir, opts.image_name)): errors.append("The disk image to be created should not exist.") # Vagrant creates a qcow2 inside a tar, turn on qcow2 if opts.make_vagrant: opts.image_type = "qcow2" # Alias --qcow2 to --image-type=qcow2 if opts.qcow2: opts.image_type = "qcow2" if opts.image_type and not os.path.exists("/usr/bin/qemu-img"): errors.append("image-type requires the qemu-img utility to be installed." % opts.image_type) if opts.image_type and opts.make_iso: errors.append("image-type cannot be used to make a bootable iso.") if opts.image_type and opts.make_fsimage: errors.append("image-type cannot be used to make filesystem images.") if opts.image_type and opts.make_tar: errors.append("image-type cannot be used to make a tar.") if opts.make_oci and not (opts.oci_config and opts.oci_runtime): errors.append("--make-oci requires --oci-config and --oci-runtime") if opts.make_oci and not os.path.exists(opts.oci_config): errors.append("oci % file is missing" % opts.oci_config) if opts.make_oci and not os.path.exists(opts.oci_runtime): errors.append("oci % file is missing" % opts.oci_runtime) if opts.make_vagrant and opts.vagrant_metadata and not os.path.exists(opts.vagrant_metadata): errors.append("Vagrant metadata file %s is missing" % opts.vagrant_metadata) if opts.virt_uefi and not os.path.isdir(opts.ovmf_path): errors.append("The OVMF firmware is missing from %s" % opts.ovmf_path) elif opts.virt_uefi and os.path.isdir(opts.ovmf_path): for f in ["OVMF_CODE.fd", "OVMF_VARS.fd"]: if not os.path.exists(joinpaths(opts.ovmf_path, f)): errors.append("OVMF firmware file %s is missing from %s" % (f, opts.ovmf_path)) if os.getuid() != 0: errors.append("You need to run this as root") if errors: list(log.error(e) for e in errors) sys.exit(1) if not os.path.exists(opts.result_dir): os.makedirs(opts.result_dir) # AMI image is just a fsimage with an AMI label if opts.make_ami: opts.make_fsimage = True if not opts.image_name: opts.image_name = "ami-root.img" if opts.fs_label == "Anaconda": opts.fs_label = "AMI" elif opts.make_tar: if not opts.image_name: opts.image_name = default_image_name(opts.compression, "root.tar") if opts.compression == "xz" and not opts.compress_args: opts.compress_args = ["-9"] elif opts.make_oci: if not opts.image_name: opts.image_name = default_image_name(opts.compression, "bundle.tar") if opts.compression == "xz" and not opts.compress_args: opts.compress_args = ["-9"] elif opts.make_vagrant: if not opts.image_name: opts.image_name = default_image_name(opts.compression, "vagrant.tar") if opts.compression == "xz" and not opts.compress_args: opts.compress_args = ["-9"] if opts.app_file: opts.app_file = joinpaths(opts.result_dir, opts.app_file) if opts.make_ostree_live: opts.make_pxe_live = True opts.ostree = True else: opts.ostree = False tempfile.tempdir = opts.tmp disk_img = None # Parse the kickstart if opts.ks: ks_version = makeVersion() ks = KickstartParser(ks_version, errorsAreFatal=False, missingIncludeIsFatal=False) ks.readKickstart(opts.ks[0]) # live iso usually needs dracut-live so warn the user if it is missing if opts.ks and opts.make_iso: if "dracut-live" not in ks.handler.packages.packageList: log.error("dracut-live package is missing from the kickstart.") sys.exit(1) # Make the disk or filesystem image if not opts.disk_image and not opts.fs_image: errors = [] if opts.no_virt and ks.handler.method.method not in ("url", "nfs") \ and not ks.handler.ostreesetup.seen: errors.append("Only url, nfs and ostreesetup install methods are currently supported." "Please fix your kickstart file." ) if ks.handler.method.method in ("url", "nfs") and not ks.handler.network.seen: errors.append("The kickstart must activate networking if " "the url or nfs install method is used.") if ks.handler.displaymode.displayMode is not None: errors.append("The kickstart must not set a display mode (text, cmdline, " "graphical), this will interfere with livemedia-creator.") if opts.make_fsimage or (opts.make_pxe_live and opts.no_virt): # Make sure the kickstart isn't using autopart and only has a / mountpoint part_ok = not any(p for p in ks.handler.partition.partitions if p.mountpoint not in ["/", "swap"]) if not part_ok or ks.handler.autopart.seen: errors.append("Filesystem images must use a single / part, not autopart or " "multiple partitions. swap is allowed but not used.") if not opts.no_virt and ks.handler.reboot.action != KS_SHUTDOWN: errors.append("The kickstart must include shutdown when using virt installation.") if errors: list(log.error(e) for e in errors) sys.exit(1) # Make the image. Output of this is either a partitioned disk image or a fsimage try: disk_img = make_image(opts, ks) except InstallError as e: log.error("ERROR: Image creation failed: %s", e) sys.exit(1) result_dir = None if not opts.image_only: if opts.make_iso: work_dir = tempfile.mkdtemp(prefix="lmc-work-") log.info("working dir is %s", work_dir) if (opts.fs_image or opts.no_virt) and not opts.disk_image: # Create iso from a filesystem image disk_img = opts.fs_image or disk_img if not make_squashfs(opts, disk_img, work_dir): log.error("squashfs.img creation failed") sys.exit(1) with Mount(disk_img, opts="loop") as mount_dir: result_dir = make_livecd(opts, mount_dir, work_dir) else: # Create iso from a partitioned disk image disk_img = opts.disk_image or disk_img with PartitionMount(disk_img) as img_mount: if img_mount and img_mount.mount_dir: make_runtime(opts, img_mount.mount_dir, work_dir, calculate_disk_size(opts, ks)/1024.0) result_dir = make_livecd(opts, img_mount.mount_dir, work_dir) # --iso-only removes the extra build artifacts, keeping only the boot.iso if opts.iso_only and result_dir: boot_iso = joinpaths(result_dir, "images/boot.iso") if not os.path.exists(boot_iso): log.error("%s is missing, skipping --iso-only.", boot_iso) else: iso_dir = tempfile.mkdtemp(prefix="lmc-result-") dest_file = joinpaths(iso_dir, opts.iso_name or "boot.iso") shutil.move(boot_iso, dest_file) shutil.rmtree(result_dir) result_dir = iso_dir # cleanup the mess # cleanup work_dir? if disk_img and not (opts.keep_image or opts.disk_image or opts.fs_image): os.unlink(disk_img) log.info("Disk image erased") disk_img = None elif opts.make_appliance: if not opts.ks: networks = [] else: networks = ks.handler.network.network make_appliance(opts.disk_image or disk_img, opts.app_name, opts.app_template, opts.app_file, networks, opts.ram, opts.vcpus or 1, opts.arch, opts.title, opts.project, opts.releasever) elif opts.make_pxe_live: work_dir = tempfile.mkdtemp(prefix="lmc-work-") log.info("working dir is %s", work_dir) disk_img = opts.fs_image or opts.disk_image or disk_img log.debug("disk image is %s", disk_img) result_dir = make_live_images(opts, work_dir, disk_img) if result_dir is None: log.error("Creating PXE live image failed.") sys.exit(1) if opts.result_dir != opts.tmp and result_dir: copytree(result_dir, opts.result_dir, preserve=False) shutil.rmtree(result_dir) result_dir = None log.info("SUMMARY") log.info("-------") log.info("Logs are in %s", os.path.abspath(os.path.dirname(opts.logfile))) if disk_img: log.info("Disk image is at %s", disk_img) if opts.make_appliance: log.info("Appliance description is in %s", opts.app_file) log.info("Results are in %s", result_dir or opts.result_dir) sys.exit( 0 ) if __name__ == '__main__': main()