ack ----- Original Message ----- > From: "Brian C. Lane" <bcl@xxxxxxxxxx> > > livecd-creator needs mkefiboot to make images that are bootable on > Mac > --- > lorax.spec | 6 +- > setup.py | 2 +- > src/pylorax/imgutils.py | 292 > +++++++++++++++++++++++++++++++++++++++++++++++ > src/sbin/mkefiboot | 111 ++++++++++++++++++ > 4 files changed, 409 insertions(+), 2 deletions(-) > create mode 100644 src/pylorax/imgutils.py > create mode 100755 src/sbin/mkefiboot > > diff --git a/lorax.spec b/lorax.spec > index ff80863..4161ee8 100644 > --- a/lorax.spec > +++ b/lorax.spec > @@ -1,7 +1,7 @@ > %define debug_package %{nil} > > Name: lorax > -Version: 16.4.7 > +Version: 16.4.8 > Release: 1%{?dist} > Summary: Tool for creating the anaconda install images > > @@ -54,6 +54,7 @@ make DESTDIR=$RPM_BUILD_ROOT install > %{python_sitelib}/pylorax > %{python_sitelib}/*.egg-info > %{_sbindir}/lorax > +%{_sbindir}/mkefiboot > %dir %{_sysconfdir}/lorax > %config(noreplace) %{_sysconfdir}/lorax/lorax.conf > %dir %{_datadir}/lorax > @@ -61,6 +62,9 @@ make DESTDIR=$RPM_BUILD_ROOT install > > > %changelog > +* Mon Mar 05 2012 Brian C. Lane <bcl@xxxxxxxxxx> 16.4.8-1 > +- Add mkefiboot and imgutils.py > + > * Mon Oct 17 2011 Martin Gracik <mgracik@xxxxxxxxxx> 16.4.7-1 > - Changes required for grub2 (dgilmore) > - Add fpaste to install environment (#727842) > diff --git a/setup.py b/setup.py > index e026651..892fb95 100644 > --- a/setup.py > +++ b/setup.py > @@ -15,7 +15,7 @@ for root, dnames, fnames in os.walk("share"): > [os.path.join(root, fname)])) > > # executable > -data_files.append(("/usr/sbin", ["src/sbin/lorax"])) > +data_files.append(("/usr/sbin", ["src/sbin/lorax", > "src/sbin/mkefiboot"])) > > setup(name="lorax", > version="0.1", > diff --git a/src/pylorax/imgutils.py b/src/pylorax/imgutils.py > new file mode 100644 > index 0000000..db344e0 > --- /dev/null > +++ b/src/pylorax/imgutils.py > @@ -0,0 +1,292 @@ > +# imgutils.py - utility functions/classes for building disk images > +# > +# Copyright (C) 2011 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 > <http://www.gnu.org/licenses/>. > +# > +# Author(s): Will Woods <wwoods@xxxxxxxxxx> > + > +import logging > +logger = logging.getLogger("pylorax.imgutils") > + > +import os, tempfile > +from os.path import join, dirname > +from pylorax.sysutils import cpfile > +from subprocess import * > +import traceback > + > +######## Functions for making container images (cpio, squashfs) > ########## > + > +def mkcpio(rootdir, outfile, compression="xz", compressargs=["-9"]): > + '''Make a compressed CPIO archive of the given rootdir. > + compression should be "xz", "gzip", "lzma", or None. > + compressargs will be used on the compression commandline.''' > + if compression not in (None, "xz", "gzip", "lzma"): > + raise ValueError, "Unknown compression type %s" % > compression > + chdir = lambda: os.chdir(rootdir) > + if compression == "xz": > + compressargs.insert(0, "--check=crc32") > + if compression is None: > + compression = "cat" # this is a little silly > + compressargs = [] > + find = Popen(["find", ".", "-print0"], stdout=PIPE, > preexec_fn=chdir) > + cpio = Popen(["cpio", "--null", "--quiet", "-H", "newc", "-o"], > + stdin=find.stdout, stdout=PIPE, preexec_fn=chdir) > + comp = Popen([compression] + compressargs, > + stdin=cpio.stdout, stdout=open(outfile, "wb")) > + comp.wait() > + return comp.returncode > + > +def mksquashfs(rootdir, outfile, compression="default", > compressargs=[]): > + '''Make a squashfs image containing the given rootdir.''' > + if compression != "default": > + compressargs = ["-comp", compression] + compressargs > + return call(["mksquashfs", rootdir, outfile] + compressargs) > + > +######## Utility functions > ############################################### > + > +def mksparse(outfile, size): > + '''use os.ftruncate to create a sparse file of the given > size.''' > + fobj = open(outfile, "w") > + os.ftruncate(fobj.fileno(), size) > + > +def loop_attach(outfile): > + '''Attach a loop device to the given file. Return the loop > device name. > + Raises CalledProcessError if losetup fails.''' > + dev = check_output(["losetup", "--find", "--show", outfile], > stderr=PIPE) > + return dev.strip() > + > +def loop_detach(loopdev): > + '''Detach the given loop device. Return False on failure.''' > + return (call(["losetup", "--detach", loopdev]) == 0) > + > +def dm_attach(dev, size, name=None): > + '''Attach a devicemapper device to the given device, with the > given size. > + If name is None, a random name will be chosen. Returns the > device name. > + raises CalledProcessError if dmsetup fails.''' > + if name is None: > + name = tempfile.mktemp(prefix="lorax.imgutils.", dir="") > + check_call(["dmsetup", "create", name, "--table", > + "0 %i linear %s 0" % (size/512, dev)], > + stdout=PIPE, stderr=PIPE) > + return name > + > +def dm_detach(dev): > + '''Detach the named devicemapper device. Returns False if > dmsetup fails.''' > + dev = dev.replace("/dev/mapper/", "") # strip prefix, if it's > there > + return call(["dmsetup", "remove", dev], stdout=PIPE, > stderr=PIPE) > + > +def mount(dev, opts="", mnt=None): > + '''Mount the given device at the given mountpoint, using the > given opts. > + opts should be a comma-separated string of mount options. > + if mnt is none, a temporary directory will be created and its > path will be > + returned. > + raises CalledProcessError if mount fails.''' > + if mnt is None: > + mnt = tempfile.mkdtemp(prefix="lorax.imgutils.") > + mount = ["mount"] > + if opts: > + mount += ["-o", opts] > + check_call(mount + [dev, mnt]) > + return mnt > + > +def umount(mnt): > + '''Unmount the given mountpoint. If the mount was a temporary > dir created > + by mount, it will be deleted. Returns false if the unmount > fails.''' > + rv = call(["umount", mnt]) > + if 'lorax.imgutils' in mnt: > + os.rmdir(mnt) > + return (rv == 0) > + > +def copytree(src, dest, preserve=True): > + '''Copy a tree of files using cp -a, thus preserving modes, > timestamps, > + links, acls, sparse files, xattrs, selinux contexts, etc. > + If preserve is False, uses cp -R (useful for modeless > filesystems)''' > + chdir = lambda: os.chdir(src) > + cp = ["cp", "-a"] if preserve else ["cp", "-R", "-L"] > + check_call(cp + [".", os.path.abspath(dest)], preexec_fn=chdir) > + > +def do_grafts(grafts, dest, preserve=True): > + '''Copy each of the items listed in grafts into dest. > + If the key ends with '/' it's assumed to be a directory which > should be > + created, otherwise just the leading directories will be > created.''' > + for imgpath, filename in grafts.items(): > + if imgpath[-1] == '/': > + targetdir = join(dest, imgpath) > + imgpath = imgpath[:-1] > + else: > + targetdir = join(dest, dirname(imgpath)) > + if not os.path.isdir(targetdir): > + os.makedirs(targetdir) > + if os.path.isdir(filename): > + copytree(filename, join(dest, imgpath), preserve) > + else: > + cpfile(filename, join(dest, imgpath)) > + > +def round_to_blocks(size, blocksize): > + '''If size isn't a multiple of blocksize, round up to the next > multiple''' > + diff = size % blocksize > + if diff or not size: > + size += blocksize - diff > + return size > + > +# TODO: move filesystem data outside this function > +def estimate_size(rootdir, graft={}, fstype=None, blocksize=4096, > overhead=128): > + getsize = lambda f: os.lstat(f).st_size > + if fstype == "btrfs": > + overhead = 64*1024 # don't worry, it's all sparse > + if fstype in ("vfat", "msdos"): > + blocksize = 2048 > + getsize = lambda f: os.stat(f).st_size # no symlinks, count > as copies > + total = overhead*blocksize > + dirlist = graft.values() > + if rootdir: > + dirlist.append(rootdir) > + for root in dirlist: > + for top, dirs, files in os.walk(root): > + for f in files + dirs: > + total += round_to_blocks(getsize(join(top,f)), > blocksize) > + if fstype == "btrfs": > + total = max(256*1024*1024, total) # btrfs minimum size: > 256MB > + return total > + > +######## Execution contexts - use with the 'with' statement > ############## > + > +class LoopDev(object): > + def __init__(self, filename, size=None): > + self.filename = filename > + if size: > + mksparse(self.filename, size) > + def __enter__(self): > + self.loopdev = loop_attach(self.filename) > + return self.loopdev > + def __exit__(self, exc_type, exc_value, traceback): > + loop_detach(self.loopdev) > + > +class DMDev(object): > + def __init__(self, dev, size, name=None): > + (self.dev, self.size, self.name) = (dev, size, name) > + def __enter__(self): > + self.mapperdev = dm_attach(self.dev, self.size, self.name) > + return self.mapperdev > + def __exit__(self, exc_type, exc_value, traceback): > + dm_detach(self.mapperdev) > + > +class Mount(object): > + def __init__(self, dev, opts="", mnt=None): > + (self.dev, self.opts, self.mnt) = (dev, opts, mnt) > + def __enter__(self): > + self.mnt = mount(self.dev, self.opts, self.mnt) > + return self.mnt > + def __exit__(self, exc_type, exc_value, traceback): > + umount(self.mnt) > + > +class PartitionMount(object): > + """ Mount a partitioned image file using kpartx """ > + def __init__(self, disk_img, mount_ok=None): > + """ > + disk_img is the full path to a partitioned disk image > + mount_ok is a function that is passed the mount point and > + returns True if it should be mounted. > + """ > + self.mount_dir = None > + self.disk_img = disk_img > + self.mount_ok = mount_ok > + > + # Default is to mount partition with /etc/passwd > + if not self.mount_ok: > + self.mount_ok = lambda mount_dir: > os.path.isfile(mount_dir+"/etc/passwd") > + > + # Example kpartx output > + # kpartx -p p -v -a /tmp/diskV2DiCW.im > + # add map loop2p1 (253:2): 0 3481600 linear /dev/loop2 2048 > + # add map loop2p2 (253:3): 0 614400 linear /dev/loop2 > 3483648 > + cmd = [ "kpartx", "-v", "-p", "p", "-a", self.disk_img ] > + logger.debug(cmd) > + kpartx_output = check_output(cmd) > + logger.debug(kpartx_output) > + > + # list of (deviceName, sizeInBytes) > + self.loop_devices = [] > + for line in kpartx_output.splitlines(): > + # add map loop2p3 (253:4): 0 7139328 linear /dev/loop2 > 528384 > + # 3rd element is size in 512 byte blocks > + if line.startswith("add map "): > + fields = line[8:].split() > + self.loop_devices.append( (fields[0], > int(fields[3])*512) ) > + > + def __enter__(self): > + # Mount the device selected by mount_ok, if possible > + mount_dir = tempfile.mkdtemp() > + for dev, size in self.loop_devices: > + try: > + mount( "/dev/mapper/"+dev, mnt=mount_dir ) > + if self.mount_ok(mount_dir): > + self.mount_dir = mount_dir > + self.mount_dev = dev > + self.mount_size = size > + break > + umount( mount_dir ) > + except CalledProcessError: > + logger.debug(traceback.format_exc()) > + if self.mount_dir: > + logger.info("Partition mounted on {0} > size={1}".format(self.mount_dir, self.mount_size)) > + else: > + logger.debug("Unable to mount anything from > {0}".format(self.disk_img)) > + os.rmdir(mount_dir) > + return self > + > + def __exit__(self, exc_type, exc_value, traceback): > + if self.mount_dir: > + umount( self.mount_dir ) > + os.rmdir(self.mount_dir) > + self.mount_dir = None > + call(["kpartx", "-d", self.disk_img]) > + > + > +######## Functions for making filesystem images > ########################## > + > +def mkfsimage(fstype, rootdir, outfile, size=None, mkfsargs=[], > mountargs="", graft={}): > + '''Generic filesystem image creation function. > + fstype should be a filesystem type - "mkfs.${fstype}" must > exist. > + graft should be a dict: {"some/path/in/image": > "local/file/or/dir"}; > + if the path ends with a '/' it's assumed to be a directory. > + Will raise CalledProcessError if something goes wrong.''' > + preserve = (fstype not in ("msdos", "vfat")) > + if not size: > + size = estimate_size(rootdir, graft, fstype) > + with LoopDev(outfile, size) as loopdev: > + check_call(["mkfs.%s" % fstype] + mkfsargs + [loopdev], > + stdout=PIPE, stderr=PIPE) > + with Mount(loopdev, mountargs) as mnt: > + if rootdir: > + copytree(rootdir, mnt, preserve) > + do_grafts(graft, mnt, preserve) > + > +# convenience functions with useful defaults > +def mkdosimg(rootdir, outfile, size=None, label="", > mountargs="shortname=winnt,umask=0077", graft={}): > + mkfsimage("msdos", rootdir, outfile, size, mountargs=mountargs, > + mkfsargs=["-n", label], graft=graft) > + > +def mkext4img(rootdir, outfile, size=None, label="", mountargs="", > graft={}): > + mkfsimage("ext4", rootdir, outfile, size, mountargs=mountargs, > + mkfsargs=["-L", label, "-b", "1024", "-m", "0"], > graft=graft) > + > +def mkbtrfsimg(rootdir, outfile, size=None, label="", mountargs="", > graft={}): > + mkfsimage("btrfs", rootdir, outfile, size, mountargs=mountargs, > + mkfsargs=["-L", label], graft=graft) > + > +def mkhfsimg(rootdir, outfile, size=None, label="", mountargs="", > graft={}): > + mkfsimage("hfsplus", rootdir, outfile, size, > mountargs=mountargs, > + mkfsargs=["-v", label], graft=graft) > diff --git a/src/sbin/mkefiboot b/src/sbin/mkefiboot > new file mode 100755 > index 0000000..84b9c5e > --- /dev/null > +++ b/src/sbin/mkefiboot > @@ -0,0 +1,111 @@ > +#!/usr/bin/python > +# mkefiboot - a tool to make EFI boot images > +# Copyright (C) 2011 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 > <http://www.gnu.org/licenses/>. > +# > +# Red Hat Author(s): Will Woods <wwoods@xxxxxxxxxx> > + > +import os, tempfile, argparse > +from subprocess import check_call, PIPE > +from pylorax.imgutils import mkdosimg, round_to_blocks, LoopDev, > DMDev, dm_detach > +from pylorax.imgutils import mkhfsimg, Mount > +import struct, shutil, glob > + > +def mkefiboot(bootdir, outfile, label): > + '''Make an EFI boot image with the contents of bootdir in > EFI/BOOT''' > + mkdosimg(None, outfile, label=label, graft={'EFI/BOOT':bootdir}) > + > +def mkmacboot(bootdir, outfile, label, icon=None): > + '''Make an EFI boot image for Apple's EFI implementation''' > + graft = {'EFI/BOOT':bootdir} > + if icon: > + graft['.VolumeIcon.icns'] = icon > + mkhfsimg(None, outfile, label=label, graft=graft) > + macbless(outfile) > + > +# To make an HFS+ image bootable, we need to fill in parts of the > +# HFSPlusVolumeHeader structure - specifically, finderInfo[0,1,5]. > +# For details, see Technical Note TN1150: HFS Plus Volume Format > +# http://developer.apple.com/library/mac/#technotes/tn/tn1150.html > +def macbless(imgfile): > + '''"bless" the EFI bootloader inside the given Mac EFI boot > image, by > + writing its inode info into the HFS+ volume header.''' > + # Get the inode number for the boot image and its parent > directory > + with LoopDev(imgfile) as loopdev: > + with Mount(loopdev) as mnt: > + loader = > glob.glob(os.path.join(mnt,'EFI/BOOT/BOOT*.efi'))[0] > + blessnode = os.stat(loader).st_ino > + dirnode = os.stat(os.path.dirname(loader)).st_ino > + # format data properly (big-endian UInt32) > + nodedata = struct.pack(">i", blessnode) > + dirdata = struct.pack(">i", dirnode) > + # Write it to the volume header > + with open(imgfile, "r+b") as img: > + img.seek(0x450) # HFSPlusVolumeHeader->finderInfo > + img.write(dirdata) # finderInfo[0] > + img.write(nodedata) # finderInfo[1] > + img.seek(0x464) # > + img.write(dirdata) # finderInfo[5] > + > +def mkefidisk(efiboot, outfile): > + '''Make a bootable EFI disk image out of the given EFI boot > image.''' > + # pjones sez: "17408 is the size of the GPT tables parted > creates" > + partsize = os.path.getsize(efiboot) + 17408 > + disksize = round_to_blocks(17408 + partsize, 512) > + with LoopDev(outfile, disksize) as loopdev: > + with DMDev(loopdev, disksize) as dmdev: > + check_call(["parted", "--script", "/dev/mapper/%s" % > dmdev, > + "mklabel", "gpt", > + "unit", "b", > + "mkpart", "'EFI System Partition'", "fat32", "17408", > str(partsize), > + "set", "1", "boot", "on"], stdout=PIPE, stderr=PIPE) > + partdev = "/dev/mapper/{0}p1".format(dmdev) > + with open(efiboot, "rb") as infile: > + with open(partdev, "wb") as outfile: > + outfile.write(infile.read()) > + dm_detach(dmdev+"p1") > + > +if __name__ == '__main__': > + parser = argparse.ArgumentParser(description="Make an EFI boot > image from the given directory.") > + parser.add_argument("-d", "--disk", action="store_true", > + help="make a full EFI disk image (including partition > table)") > + parser.add_argument("-a", "--apple", action="store_const", > const="apple", > + dest="imgtype", default="default", > + help="make an Apple EFI image (use hfs+, bless bootloader)") > + parser.add_argument("-l", "--label", default="EFI", > + help="filesystem label to use (default: %(default)s)") > + parser.add_argument("-i", "--icon", metavar="ICONFILE", > + help="icon file to include (for Apple EFI image)") > + parser.add_argument("bootdir", metavar="EFIBOOTDIR", > + help="input directory (will become /EFI/BOOT in the image)") > + parser.add_argument("outfile", metavar="OUTPUTFILE", > + help="output file to write") > + opt = parser.parse_args() > + # sanity checks > + if not os.path.isdir(opt.bootdir): > + parser.error("%s is not a directory" % opt.bootdir) > + if os.getuid() > 0: > + parser.error("need root permissions") > + if opt.icon and not opt.imgtype == "apple": > + print "Warning: --icon is only useful for Apple EFI images" > + # do the thing! > + if opt.imgtype == "apple": > + mkmacboot(opt.bootdir, opt.outfile, opt.label, opt.icon) > + else: > + mkefiboot(opt.bootdir, opt.outfile, opt.label) > + if opt.disk: > + efiboot = > tempfile.NamedTemporaryFile(prefix="mkefiboot.").name > + shutil.move(opt.outfile, efiboot) > + mkefidisk(efiboot, opt.outfile) > -- > 1.7.7.6 > > _______________________________________________ > Anaconda-devel-list mailing list > Anaconda-devel-list@xxxxxxxxxx > https://www.redhat.com/mailman/listinfo/anaconda-devel-list > _______________________________________________ Anaconda-devel-list mailing list Anaconda-devel-list@xxxxxxxxxx https://www.redhat.com/mailman/listinfo/anaconda-devel-list