diff --git a/README.livemedia-creator b/README.livemedia-creator index 419dce3a..05357acd 100644 --- a/README.livemedia-creator +++ b/README.livemedia-creator @@ -256,6 +256,22 @@ eg. livemedia-creator --make-tar --iso=/path/to/boot.iso --ks=./docs/fedora-minimal.ks \ --image-name=fedora-root.tar.xz +LIVE IMAGE FOR PXE BOOT +----------------------- + +The --make-pxe-live command will produce squashfs image containing live root +filesystem that can be used for pxe boot. Directory with results will contain +the live image, kernel image, initrd image and template of pxe configuration +for the images. + +ATOMIC LIVE IMAGE FOR PXE BOOT +------------------------------ + +The --make-ostree-live command will produce the same result as --make-pxe-live +for installations of Atomic Host. Example kickstart for such an installation +using Atomic installer iso with local repo included in the image can be found +in docs/rhel-atomic-pxe-live.ks. + DEBUGGING PROBLEMS ------------------ diff --git a/docs/livemedia-creator.1 b/docs/livemedia-creator.1 index 3b48d1b8..9d1379e6 100644 --- a/docs/livemedia-creator.1 +++ b/docs/livemedia-creator.1 @@ -4,7 +4,7 @@ livemedia-creator \- Create live install media .SH SYNOPSIS livemedia-creator [-h] - (--make-iso | --make-disk | --make-fsimage | --make-appliance | --make-ami | --make-tar) + (--make-iso | --make-disk | --make-fsimage | --make-appliance | --make-ami | --make-tar | --make-pxe-live | --make-ostree-live) [--iso ISO] [--disk-image DISK_IMAGE] [--fs-image FS_IMAGE] [--ks KS] [--image-name IMAGE_NAME] [--image-only] @@ -70,6 +70,14 @@ Build an ami image \fB\-\-make\-tar\fR Build a tar of the root filesystem. Defaults to root.tar.xz +.TP +\fB\-\-make\-pxe\-live\fR +Build a live pxe boot squashfs image + +.TP +\fB\-\-make\-ostree\-live\fR +Build a live pxe boot squashfs image of Atomic Host + .TP \fB\-\-iso ISO\fR Anaconda installation .iso path to use for virt-install diff --git a/docs/rhel-atomic-pxe-live.ks b/docs/rhel-atomic-pxe-live.ks new file mode 100644 index 00000000..b5d90f25 --- /dev/null +++ b/docs/rhel-atomic-pxe-live.ks @@ -0,0 +1,26 @@ +# Settings for unattended installation: +lang en_US.UTF-8 +keyboard us +timezone America/New_York +zerombr +clearpart --all --initlabel +rootpw --plaintext atomic +network --bootproto=dhcp --device=link --activate + +# We are only able to install atomic with separate /boot partition currently +part / --fstype="ext4" --size=6000 +part /boot --size=500 --fstype="ext4" + +shutdown + +services --disabled=cloud-init,cloud-init-local,cloud-final,cloud-config,docker-storage-setup + +# Using ostree repo included in installation iso. Respective ostreesetup command is included here. +# The included kickstart file with the command is created during installation iso compose. +%include /usr/share/anaconda/interactive-defaults.ks + +# We copy content of separate /boot partition to root part when building live squashfs image, +# and we don't want systemd to try to mount it when pxe booting +%post +cat /dev/null > /etc/fstab +%end diff --git a/share/pxe-live/pxe-config.tmpl b/share/pxe-live/pxe-config.tmpl new file mode 100644 index 00000000..6b841bbc --- /dev/null +++ b/share/pxe-live/pxe-config.tmpl @@ -0,0 +1,3 @@ +# PXE configuration template generated by livemedia-creator +kernel ${kernel} +append initrd=${initrd} root=live:/${liveimg} ${addargs} diff --git a/src/pylorax/imgutils.py b/src/pylorax/imgutils.py index 01b52156..f2702e41 100644 --- a/src/pylorax/imgutils.py +++ b/src/pylorax/imgutils.py @@ -91,6 +91,30 @@ def mksquashfs(rootdir, outfile, compression="default", compressargs=None): compressargs = ["-comp", compression] + compressargs return execWithRedirect("mksquashfs", [rootdir, outfile] + compressargs) +def mkrootfsimg(rootdir, outfile, label, size=2, sysroot=""): + """ + Make rootfs image from a directory + + :param str rootdir: Root directory + :param str outfile: Path of output image file + :param str label: Filesystem label + :param int size: Size of the image, if None computed automatically + :param str sysroot: path to system (deployment) root relative to physical root + """ + if size: + fssize = size * (1024*1024*1024) # 2GB sparse file compresses down to nothin' + else: + fssize = None # Let mkext4img figure out the needed size + + mkext4img(rootdir, outfile, label=label, size=fssize) + # Reset selinux context on new rootfs + with LoopDev(outfile) as loopdev: + with Mount(loopdev) as mnt: + cmd = [ "setfiles", "-e", "/proc", "-e", "/sys", "-e", "/dev", "-e", "/install", + "/etc/selinux/targeted/contexts/files/file_contexts", "/"] + root = join(mnt, sysroot.lstrip("/")) + runcmd(cmd, root=root) + ######## Utility functions ############################################### def mksparse(outfile, size): diff --git a/src/pylorax/treebuilder.py b/src/pylorax/treebuilder.py index a2207072..baad1ac5 100644 --- a/src/pylorax/treebuilder.py +++ b/src/pylorax/treebuilder.py @@ -160,20 +160,10 @@ class RuntimeBuilder(object): # make live rootfs image - must be named "LiveOS/rootfs.img" for dracut compressargs = compressargs or [] workdir = joinpaths(os.path.dirname(outfile), "runtime-workdir") - if size: - fssize = size * (1024*1024*1024) # 2GB sparse file compresses down to nothin' - else: - fssize = None # Let mkext4img figure out the needed size os.makedirs(joinpaths(workdir, "LiveOS")) - imgutils.mkext4img(self.vars.root, joinpaths(workdir, "LiveOS/rootfs.img"), - label="Anaconda", size=fssize) - # Reset selinux context on new rootfs - with imgutils.LoopDev( joinpaths(workdir, "LiveOS/rootfs.img") ) as loopdev: - with imgutils.Mount(loopdev) as mnt: - cmd = [ "setfiles", "-e", "/proc", "-e", "/sys", "-e", "/dev", "-e", "/install", - "/etc/selinux/targeted/contexts/files/file_contexts", "/"] - runcmd(cmd, root=mnt) + imgutils.mkrootfsimg(self.vars.root, joinpaths(workdir, "LiveOS/rootfs.img"), + "Anaconda", size=size) # squash the live rootfs and clean up workdir imgutils.mksquashfs(workdir, outfile, compression, compressargs) diff --git a/src/sbin/livemedia-creator b/src/sbin/livemedia-creator index b21cdbe3..a3286233 100755 --- a/src/sbin/livemedia-creator +++ b/src/sbin/livemedia-creator @@ -37,6 +37,7 @@ import shutil import argparse import hashlib import re +import glob # Use pykickstart to calculate disk image size from pykickstart.parser import KickstartParser @@ -54,8 +55,8 @@ from pylorax.treebuilder import findkernels from pylorax.sysutils import joinpaths, remove from pylorax.imgutils import PartitionMount, mksparse, mkext4img, loop_detach from pylorax.imgutils import get_loop_name, dm_detach, mount, umount, Mount -from pylorax.imgutils import mksquashfs, mkqcow2, mktar -from pylorax.executils import execWithRedirect, execWithCapture +from pylorax.imgutils import mksquashfs, mkqcow2, mktar, mkrootfsimg +from pylorax.executils import execWithRedirect, execWithCapture, runcmd # no-virt mode doesn't need libvirt, so make it optional try: @@ -420,6 +421,22 @@ def is_image_mounted(disk_img): return True return False +def find_ostree_root(phys_root): + """ + Find root of ostree deployment + + :param str phys_root: Path to physical root + :returns: Relative path of ostree deployment root + :rtype: str + :raise Exception: More than one deployment roots were found + """ + ostree_root = "" + ostree_sysroots = glob.glob(joinpaths(phys_root, "ostree/boot.0/*/*/0")) + if ostree_sysroots: + if len(ostree_sysroots) > 1: + raise Exception("Too many deployment roots found: %s" % ostree_sysroots) + ostree_root = os.path.relpath(ostree_sysroots[0], phys_root) + return ostree_root def get_arch(mount_dir): """ @@ -533,6 +550,99 @@ def make_runtime(opts, mount_dir, work_dir): log.info("Creating runtime") rb.create_runtime(joinpaths(work_dir, RUNTIME), size=None) +def rebuild_initrds_for_live(opts, sys_root_dir, results_dir): + """ + Rebuild intrds for pxe live image (root=live:http://) + + :param opts: options passed to livemedia-creator + :type opts: argparse options + :param str sys_root_dir: Path to root of the system + :param str results_dir: Path of directory for storing results + """ + if not opts.dracut_args: + dracut_args = DRACUT_DEFAULT + else: + dracut_args = [] + for arg in opts.dracut_args: + dracut_args += arg.split(" ", 1) + log.info("dracut args = {0}".format(dracut_args)) + + dracut = ["dracut", "--nomdadmconf", "--nolvmconf"] + dracut_args + + kdir = "boot" + if opts.ostree: + kernels_dir = glob.glob(joinpaths(sys_root_dir, "boot/ostree/*"))[0] + kdir = os.path.relpath(kernels_dir, sys_root_dir) + + kernels = [kernel for kernel in findkernels(sys_root_dir, kdir) + if hasattr(kernel, "initrd")] + if not kernels: + raise Exception("No initrds found, cannot rebuild_initrds") + + # Hush some dracut warnings. TODO: bind-mount proc in place? + open(joinpaths(sys_root_dir,"/proc/modules"),"w") + + if opts.ostree: + # Dracut assumes to have some dirs in disk image + # /var/tmp for temp files + vartmp_dir = joinpaths(sys_root_dir, "var/tmp") + if not os.path.isdir(vartmp_dir): + os.mkdir(vartmp_dir) + # /root (maybe not fatal) + root_dir = joinpaths(sys_root_dir, "var/roothome") + if not os.path.isdir(root_dir): + os.mkdir(root_dir) + # /tmp (maybe not fatal) + tmp_dir = joinpaths(sys_root_dir, "sysroot/tmp") + if not os.path.isdir(tmp_dir): + os.mkdir(tmp_dir) + + for kernel in kernels: + outfile = kernel.initrd.path + ".live" + log.info("rebuilding %s", outfile) + + kver = kernel.version + + cmd = dracut + [outfile, kver] + runcmd(cmd, root=sys_root_dir) + + new_initrd_path = joinpaths(results_dir, os.path.basename(kernel.initrd.path)) + shutil.move(joinpaths(sys_root_dir, outfile), new_initrd_path) + shutil.copy2(joinpaths(sys_root_dir, kernel.path), results_dir) + + os.unlink(joinpaths(sys_root_dir,"/proc/modules")) + +def create_pxe_config(template, images_dir, live_image_name, add_args = None): + """ + Create template for pxe to live configuration + + :param str images_dir: Path of directory with images to be used + :param str live_image_name: Name of live rootfs image file + :param list add_args: Arguments to be added to initrd= pxe config + """ + + add_args = add_args or [] + + kernels = [kernel for kernel in findkernels(images_dir, kdir="") + if hasattr(kernel, "initrd")] + if not kernels: + return + + kernel = kernels[0] + + add_args_str = " ".join(add_args) + + + try: + result = Template(filename=template).render(kernel=kernel.path, + initrd=kernel.initrd.path, liveimg=live_image_name, + addargs=add_args_str) + except Exception: + log.error(text_error_template().render()) + raise + + with open (joinpaths(images_dir, "PXE_CONFIG"), "w") as f: + f.write(result) def make_livecd(opts, mount_dir, work_dir): """ @@ -594,6 +704,36 @@ def make_livecd(opts, mount_dir, work_dir): return work_dir +def mount_boot_part_over_root(img_mount): + """ + Mount boot partition to /boot of root fs mounted in img_mount + + Used for OSTree so it finds deployment configurations on live rootfs + + param img_mount: object with mounted disk image root partition + type img_mount: imgutils.PartitionMount + """ + root_dir = img_mount.mount_dir + is_boot_part = lambda dir: os.path.exists(dir+"/loader.0") + tmp_mount_dir = tempfile.mkdtemp() + sys_root = find_ostree_root(root_dir) + sysroot_boot_dir = None + for dev, _size in img_mount.loop_devices: + if dev is img_mount.mount_dev: + continue + try: + mount("/dev/mapper/"+dev, mnt=tmp_mount_dir) + if is_boot_part(tmp_mount_dir): + umount(tmp_mount_dir) + sysroot_boot_dir = joinpaths(joinpaths(root_dir, sys_root), "boot") + mount("/dev/mapper/"+dev, mnt=sysroot_boot_dir) + break + else: + umount(tmp_mount_dir) + except subprocess.CalledProcessError as e: + log.debug("Looking for boot partition error: %s", e) + remove(tmp_mount_dir) + return sysroot_boot_dir def novirt_log_check(log_check, proc): """ @@ -892,6 +1032,55 @@ def make_image(opts, ks): return disk_img +def make_live_images(opts, work_dir, root_dir, rootfs_image=None): + """ + Create live images from direcory or rootfs image + + :param opts: options passed to livemedia-creator + :type opts: argparse options + :param str work_dir: Directory for storing results + :param str root_dir: Root directory of live filesystem tree + :param str rootfs_image: Path to live rootfs image to be used + :returns: Path of directory with created images + :rtype: str + """ + sys_root = "" + if opts.ostree: + sys_root = find_ostree_root(root_dir) + + squashfs_root_dir = joinpaths(work_dir, "squashfs_root") + liveos_dir = joinpaths(squashfs_root_dir, "LiveOS") + os.makedirs(liveos_dir) + + if rootfs_image: + rc = execWithRedirect("/bin/ln", [rootfs_image, joinpaths(liveos_dir, "rootfs.img")]) + if rc != 0: + shutil.copy2(rootfs_image, joinpaths(liveos_dir, "rootfs.img")) + else: + log.info("Creating live rootfs image") + mkrootfsimg(root_dir, joinpaths(liveos_dir, "rootfs.img"), "LiveOS", size=None, sysroot=sys_root) + + log.info("Packing live rootfs image") + add_pxe_args = [] + live_image_name = "live-rootfs.squashfs.img" + mksquashfs(squashfs_root_dir, + joinpaths(work_dir, live_image_name), + opts.compression, + opts.compress_args) + + remove(squashfs_root_dir) + + log.info("Rebuilding initramfs for live") + rebuild_initrds_for_live(opts, joinpaths(root_dir, sys_root), work_dir) + + if opts.ostree: + add_pxe_args.append("ostree=/%s" % sys_root) + template = joinpaths(opts.lorax_templates, "pxe-live/pxe-config.tmpl") + create_pxe_config(template, work_dir, live_image_name, add_pxe_args) + + return work_dir + + def setup_logging(opts): """ Setup the various logs @@ -943,6 +1132,10 @@ def main(): help="Build an ami image") action.add_argument("--make-tar", action="store_true", help="Build a tar of the root filesystem") + action.add_argument("--make-pxe-live", action="store_true", + help="Build a live pxe boot squashfs image") + action.add_argument("--make-ostree-live", action="store_true", + help="Build a live pxe boot squashfs image of Atomic Host") parser.add_argument("--iso", type=os.path.abspath, help="Anaconda installation .iso path to use for virt-install") @@ -1142,6 +1335,12 @@ def main(): if opts.app_file: opts.app_file = joinpaths(opts.tmp, 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 @@ -1216,6 +1415,31 @@ def main(): make_appliance(opts.disk_image or disk_img, opts.app_name, opts.app_template, opts.app_file, networks, opts.ram, opts.vcpus, opts.arch, opts.title, opts.project, opts.releasever) + elif opts.make_pxe_live: + work_dir = tempfile.mkdtemp() + log.info("working dir is {0}".format(work_dir)) + + if (opts.fs_image or opts.no_virt) and not opts.disk_image: + # Create pxe live images from a filesystem image + disk_img = opts.fs_image or disk_img + with Mount(disk_img, opts="loop") as mnt_dir: + result_dir = make_live_images(opts, work_dir, mnt_dir, rootfs_image=disk_img) + else: + # Create pxe live images from a partitioned disk image + disk_img = opts.disk_image or disk_img + is_root_part = None + if opts.ostree: + is_root_part = lambda dir: os.path.exists(dir+"/ostree/deploy") + with PartitionMount(disk_img, mount_ok=is_root_part) as img_mount: + if img_mount and img_mount.mount_dir: + try: + mounted_sysroot_boot_dir = None + if opts.ostree: + mounted_sysroot_boot_dir = mount_boot_part_over_root(img_mount) + result_dir = make_live_images(opts, work_dir, img_mount.mount_dir) + finally: + if mounted_sysroot_boot_dir: + umount(mounted_sysroot_boot_dir) if opts.result_dir and result_dir: shutil.copytree(result_dir, opts.result_dir)