add mkefiboot and imgutils.py
livecd-creator needs mkefiboot to make images that are bootable on Mac
This commit is contained in:
parent
1e28497ee2
commit
88c85cf76f
2
setup.py
2
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",
|
||||
|
292
src/pylorax/imgutils.py
Normal file
292
src/pylorax/imgutils.py
Normal file
@ -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)
|
111
src/sbin/mkefiboot
Executable file
111
src/sbin/mkefiboot
Executable file
@ -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)
|
Loading…
Reference in New Issue
Block a user