imgutils.py: utilities for creating disk images
This contains simple functions for creating disk images: mkcpio, mksquashfs, mkdosimg, mkext4img, mkbtrfsimg And the helper functions they use: truncate, loop_{attach,detach}, dm_{attach,detach}, mount/umount, estimate_size, roundup, cpio_copytree
This commit is contained in:
parent
1e550f8227
commit
b2b1c36167
218
src/pylorax/imgutils.py
Normal file
218
src/pylorax/imgutils.py
Normal file
@ -0,0 +1,218 @@
|
||||
# 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 subprocess import *
|
||||
|
||||
######## 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="xz", compressargs=[]):
|
||||
'''Make a squashfs image containing the given rootdir.'''
|
||||
return call(["mksquashfs", rootdir, outfile,
|
||||
"-comp", compression] + 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)
|
||||
copytree(filename, join(dest, imgpath), preserve)
|
||||
|
||||
def round_to_blocks(size, blocksize):
|
||||
'''Round the size of a file to the size of the blocks it would use'''
|
||||
diff = size % blocksize
|
||||
if diff:
|
||||
size += blocksize - diff
|
||||
return size
|
||||
|
||||
def estimate_size(rootdir, fstype=None, blocksize=4096, overhead=1024):
|
||||
if not rootdir:
|
||||
return 0
|
||||
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"):
|
||||
overhead = 32
|
||||
getsize = lambda f: os.stat(f).st_size # no symlinks, count as copies
|
||||
total = overhead*blocksize
|
||||
for root, dirs, files in os.walk(rootdir):
|
||||
for f in files:
|
||||
total += round_to_blocks(getsize(join(root,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)
|
||||
|
||||
######## 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, fstype)
|
||||
for f in graft.values():
|
||||
size += estimate_size(f, 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=0777", 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)
|
Loading…
Reference in New Issue
Block a user