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