diff --git a/setup.py b/setup.py
index e0266512..892fb95c 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 00000000..db344e0d
--- /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 .
+#
+# Author(s): Will Woods
+
+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 00000000..84b9c5e0
--- /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 .
+#
+# Red Hat Author(s): Will Woods
+
+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)