[Date Prev][Date Next]   [Thread Prev][Thread Next]   [Thread Index] [Date Index] [Author Index]

[LORAX] add mkefiboot and imgutils.py



From: "Brian C. Lane" <bcl redhat com>

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 redhat com> 16.4.8-1
+- Add mkefiboot and imgutils.py
+
 * Mon Oct 17 2011 Martin Gracik <mgracik redhat com> 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 redhat com>
+
+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 redhat com>
+
+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


[Date Prev][Date Next]   [Thread Prev][Thread Next]   [Thread Index] [Date Index] [Author Index]