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