v2 is addressing Brian's review remarks. Also example kickstart is modified
to use local repo from Atomic installer iso.
--make-pxe-live target generate live squashfs and initrd for pxe boot.
Also generates pxe config template.
--make-ostree-live is used for installations of Atomic Host. Additionally to
--make-pxe-live it ensures using deployment root instead of physical root of
installed disk image where needed. Atomic installation needs to be virt
installation with /boot on separate partition (the only way supported by
Anaconda currently). Content of boot partition is added to live root fs so that
ostree can find deployment by boot configuration.
---
README.livemedia-creator | 16 +++
docs/livemedia-creator.1 | 10 +-
docs/rhel-atomic-pxe-live.ks | 28 +++++
share/pxe-live/pxe-config.tmpl | 3 +
src/pylorax/imgutils.py | 24 +++++
src/pylorax/treebuilder.py | 16 +--
src/sbin/livemedia-creator | 227 ++++++++++++++++++++++++++++++++++++++++-
7 files changed, 307 insertions(+), 17 deletions(-)
create mode 100644 docs/rhel-atomic-pxe-live.ks
create mode 100644 share/pxe-live/pxe-config.tmpl
diff --git a/README.livemedia-creator b/README.livemedia-creator
index 419dce3..05357ac 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 1b0ad20..32a9fe5 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]
@@ -71,6 +71,14 @@ Build an ami image
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 0000000..1d8f63f
--- /dev/null
+++ b/docs/rhel-atomic-pxe-live.ks
@@ -0,0 +1,28 @@
+# 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 0000000..6b841bb
--- /dev/null
+++ b/share/pxe-live/pxe-config.tmpl
@@ -0,0 +1,3 @@
+# PXE configuration template generated by livemedia-creator
+kernel <PXE_DIR>${kernel}
+append initrd=<PXE_DIR>${initrd} root=live:<URL>/${liveimg} ${addargs}
diff --git a/src/pylorax/imgutils.py b/src/pylorax/imgutils.py
index 57d62fb..da58c17 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 12eb348..f51980e 100644
--- a/src/pylorax/treebuilder.py
+++ b/src/pylorax/treebuilder.py
@@ -156,20 +156,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 e25d422..8b8cf80 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
@@ -53,8 +54,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:
@@ -414,6 +415,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):
"""
@@ -499,7 +516,6 @@ def make_fsimage(diskimage, fsimage, img_size=None, label="Anaconda"):
mkext4img(img_mount.mount_dir, fsimage, size=img_size, label=label)
-
def make_runtime(opts, mount_dir, work_dir):
"""
Make the squashfs image from a directory
@@ -527,6 +543,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):
"""
@@ -588,6 +697,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_install(opts, disk_img, disk_size, repo_url):
"""
@@ -840,6 +979,53 @@ def make_image(opts, ks):
log.info("Disk Image install successful")
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):
"""
@@ -892,6 +1078,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")
@@ -1091,6 +1281,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
@@ -1165,6 +1361,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)
--
1.9.3