3 # Copyright (c) 2007, Red Hat, Inc.
4 # Copyright (c) 2009, 2010, 2011 Intel, Inc.
6 # This program is free software; you can redistribute it and/or modify it
7 # under the terms of the GNU General Public License as published by the Free
8 # Software Foundation; version 2 of the License
10 # This program is distributed in the hope that it will be useful, but
11 # WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
12 # or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
15 # You should have received a copy of the GNU General Public License along
16 # with this program; if not, write to the Free Software Foundation, Inc., 59
17 # Temple Place - Suite 330, Boston, MA 02111-1307, USA.
19 from __future__ import with_statement
29 from mic.utils import runner
30 from mic.utils.errors import *
33 def find_binary_inchroot(binary, chroot):
41 bin_path = "%s/%s" % (path, binary)
42 if os.path.exists("%s/%s" % (chroot, bin_path)):
46 def find_binary_path(binary):
47 if os.environ.has_key("PATH"):
48 paths = os.environ["PATH"].split(":")
51 if os.environ.has_key("HOME"):
52 paths += [os.environ["HOME"] + "/bin"]
53 paths += ["/usr/local/sbin", "/usr/local/bin", "/usr/sbin", "/usr/bin", "/sbin", "/bin"]
56 bin_path = "%s/%s" % (path, binary)
57 if os.path.exists(bin_path):
59 raise CreatorError("Command '%s' is not available." % binary)
61 def makedirs(dirname):
62 """A version of os.makedirs() that doesn't throw an
63 exception if the leaf directory already exists.
68 if err.errno != errno.EEXIST:
71 def mksquashfs(in_img, out_img):
72 fullpathmksquashfs = find_binary_path("mksquashfs")
73 args = [fullpathmksquashfs, in_img, out_img]
75 if not sys.stdout.isatty():
76 args.append("-no-progress")
78 ret = runner.show(args)
80 raise SquashfsError("'%s' exited with error (%d)" % (' '.join(args), ret))
82 def resize2fs(fs, size):
83 resize2fs = find_binary_path("resize2fs")
85 # it means to minimalize it
86 return runner.show([resize2fs, '-M', fs])
88 return runner.show([resize2fs, fs, "%sK" % (size / 1024,)])
91 fuser = find_binary_path("fuser")
92 if not os.path.exists(fp):
95 rc = runner.quiet([fuser, "-s", fp])
97 for pid in runner.outs([fuser, fp]).split():
98 fd = open("/proc/%s/cmdline" % pid, "r")
101 if cmdline[:-1] == "/bin/bash":
107 class BindChrootMount:
108 """Represents a bind mount of a directory into a chroot."""
109 def __init__(self, src, chroot, dest = None, option = None):
110 self.root = os.path.abspath(os.path.expanduser(chroot))
113 self.origsrc = self.src = src
114 if os.path.islink(src):
115 self.src = os.readlink(src)
119 self.dest = os.path.join(self.root, dest.lstrip('/'))
122 self.mountcmd = find_binary_path("mount")
123 self.umountcmd = find_binary_path("umount")
126 with open('/proc/mounts') as f:
128 if line.split()[1] == os.path.abspath(self.dest):
133 def has_chroot_instance(self):
134 lock = os.path.join(self.root, ".chroot.lock")
135 return my_fuser(lock)
138 if self.mounted or self.ismounted():
142 rc = runner.show([self.mountcmd, "--bind", self.src, self.dest])
144 raise MountError("Bind-mounting '%s' to '%s' failed" %
145 (self.src, self.dest))
147 rc = runner.show([self.mountcmd, "--bind", "-o", "remount,%s" % self.option, self.dest])
149 raise MountError("Bind-remounting '%s' failed" % self.dest)
152 if os.path.islink(self.orig_src):
153 dest = os.path.join(self.root, self.orig_src.lstrip('/'))
154 if os.path.exists(dest):
156 os.symlink(self.src, dest)
159 if self.has_chroot_instance():
163 runner.show([self.umountcmd, "-l", self.dest])
167 """LoopbackMount compatibility layer for old API"""
168 def __init__(self, lofile, mountdir, fstype = None):
169 self.diskmount = DiskMount(LoopbackDisk(lofile,size = 0),mountdir,fstype,rmmountdir = True)
171 self.losetupcmd = find_binary_path("losetup")
174 self.diskmount.cleanup()
177 self.diskmount.unmount()
181 runner.show([self.losetupcmd, "-d", self.loopdev])
189 self.loopdev = get_loop_device(self.losetupcmd, self.lofile)
193 self.diskmount.mount()
195 class SparseLoopbackMount(LoopbackMount):
196 """SparseLoopbackMount compatibility layer for old API"""
197 def __init__(self, lofile, mountdir, size, fstype = None):
198 self.diskmount = DiskMount(SparseLoopbackDisk(lofile,size),mountdir,fstype,rmmountdir = True)
200 def expand(self, create = False, size = None):
201 self.diskmount.disk.expand(create, size)
203 def truncate(self, size = None):
204 self.diskmount.disk.truncate(size)
207 self.diskmount.disk.create()
209 class SparseExtLoopbackMount(SparseLoopbackMount):
210 """SparseExtLoopbackMount compatibility layer for old API"""
211 def __init__(self, lofile, mountdir, size, fstype, blocksize, fslabel):
212 self.diskmount = ExtDiskMount(SparseLoopbackDisk(lofile,size), mountdir, fstype, blocksize, fslabel, rmmountdir = True)
215 def __format_filesystem(self):
216 self.diskmount.__format_filesystem()
219 self.diskmount.disk.create()
221 def resize(self, size = None):
222 return self.diskmount.__resize_filesystem(size)
225 self.diskmount.mount()
228 self.extdiskmount.__fsck()
230 def __get_size_from_filesystem(self):
231 return self.diskmount.__get_size_from_filesystem()
233 def __resize_to_minimal(self):
234 return self.diskmount.__resize_to_minimal()
236 def resparse(self, size = None):
237 return self.diskmount.resparse(size)
240 """Generic base object for a disk
242 The 'create' method must make the disk visible as a block device - eg
243 by calling losetup. For RawDisk, this is obviously a no-op. The 'cleanup'
244 method must undo the 'create' operation.
246 def __init__(self, size, device = None):
247 self._device = device
256 def get_device(self):
258 def set_device(self, path):
260 device = property(get_device, set_device)
264 size = property(get_size)
268 """A Disk backed by a block device.
269 Note that create() is a no-op.
271 def __init__(self, size, device):
272 Disk.__init__(self, size, device)
280 class LoopbackDisk(Disk):
281 """A Disk backed by a file via the loop module."""
282 def __init__(self, lofile, size):
283 Disk.__init__(self, size)
285 self.losetupcmd = find_binary_path("losetup")
291 return os.path.exists(self.lofile)
294 if self.device is not None:
297 self.device = get_loop_device(self.losetupcmd, self.lofile)
300 if self.device is None:
302 msger.debug("Losetup remove %s" % self.device)
303 rc = runner.show([self.losetupcmd, "-d", self.device])
306 class SparseLoopbackDisk(LoopbackDisk):
307 """A Disk backed by a sparse file via the loop module."""
308 def __init__(self, lofile, size):
309 LoopbackDisk.__init__(self, lofile, size)
311 def expand(self, create = False, size = None):
315 if not os.path.exists(self.lofile):
316 makedirs(os.path.dirname(self.lofile))
321 msger.debug("Extending sparse file %s to %d" % (self.lofile, size))
323 fd = os.open(self.lofile, flags, 0644)
325 fd = os.open(self.lofile, flags)
330 os.ftruncate(fd, size)
332 # may be limited by 2G in 32bit env
333 os.ftruncate(fd, 2**31L)
337 def truncate(self, size = None):
341 msger.debug("Truncating sparse file %s to %d" % (self.lofile, size))
342 fd = os.open(self.lofile, os.O_WRONLY)
343 os.ftruncate(fd, size)
347 self.expand(create = True)
348 LoopbackDisk.create(self)
351 """A generic base class to deal with mounting things."""
352 def __init__(self, mountdir):
353 self.mountdir = mountdir
358 def mount(self, options = None):
364 class DiskMount(Mount):
365 """A Mount object that handles mounting of a Disk."""
366 def __init__(self, disk, mountdir, fstype = None, rmmountdir = True):
367 Mount.__init__(self, mountdir)
371 self.rmmountdir = rmmountdir
376 self.mkfscmd = find_binary_path("mkfs." + self.fstype)
379 self.mountcmd = find_binary_path("mount")
380 self.umountcmd = find_binary_path("umount")
388 msger.debug("Unmounting directory %s" % self.mountdir)
389 runner.quiet('sync') # sync the data on this mount point
390 rc = runner.show([self.umountcmd, "-l", self.mountdir])
394 raise MountError("Failed to umount %s" % self.mountdir)
395 if self.rmdir and not self.mounted:
397 os.rmdir(self.mountdir)
407 def mount(self, options = None):
411 if not os.path.isdir(self.mountdir):
412 msger.debug("Creating mount point %s" % self.mountdir)
413 os.makedirs(self.mountdir)
414 self.rmdir = self.rmmountdir
418 msger.debug("Mounting %s at %s" % (self.disk.device, self.mountdir))
420 args = [ self.mountcmd, "-o", options, self.disk.device, self.mountdir ]
422 args = [ self.mountcmd, self.disk.device, self.mountdir ]
424 args.extend(["-t", self.fstype])
426 rc = runner.show(args)
428 raise MountError("Failed to mount '%s' to '%s' with command '%s'. Retval: %s" %
429 (self.disk.device, self.mountdir, " ".join(args), rc))
433 class ExtDiskMount(DiskMount):
434 """A DiskMount object that is able to format/resize ext[23] filesystems."""
435 def __init__(self, disk, mountdir, fstype, blocksize, fslabel, rmmountdir=True, skipformat = False, fsopts = None):
436 DiskMount.__init__(self, disk, mountdir, fstype, rmmountdir)
437 self.blocksize = blocksize
438 self.fslabel = fslabel.replace("/", "")
440 self.skipformat = skipformat
443 self.dumpe2fs = find_binary_path("dumpe2fs")
444 self.tune2fs = find_binary_path("tune2fs")
446 def __parse_field(self, output, field):
447 for line in output.split("\n"):
448 if line.startswith(field + ":"):
449 return line[len(field) + 1:].strip()
451 raise KeyError("Failed to find field '%s' in output" % field)
453 def __format_filesystem(self):
455 msger.debug("Skip filesystem format.")
458 msger.verbose("Formating %s filesystem on %s" % (self.fstype, self.disk.device))
459 cmdlist = [self.mkfscmd, "-F", "-L", self.fslabel, "-m", "1", "-b",
462 cmdlist.extend(self.extopts.split())
463 cmdlist.extend([self.disk.device])
465 rc, errout = runner.runtool(cmdlist, catch=2)
467 raise MountError("Error creating %s filesystem on disk %s:\n%s" %
468 (self.fstype, self.disk.device, errout))
471 msger.debug("Tuning filesystem on %s" % self.disk.device)
472 runner.show([self.tune2fs, "-c0", "-i0", "-Odir_index", "-ouser_xattr,acl", self.disk.device])
474 rc, out = runner.runtool([self.dumpe2fs, '-h', self.disk.device],
477 raise MountError("Error dumpe2fs %s filesystem on disk %s:\n%s" %
478 (self.fstype, self.disk.device, out))
479 # FIXME: specify uuid in mkfs parameter
481 self.uuid = self.__parse_field(out, "Filesystem UUID")
485 def __resize_filesystem(self, size = None):
486 current_size = os.stat(self.disk.lofile)[stat.ST_SIZE]
489 size = self.disk.size
491 if size == current_size:
494 if size > current_size:
495 self.disk.expand(size)
499 resize2fs(self.disk.lofile, size)
504 if not self.disk.fixed() and self.disk.exists():
510 self.__resize_filesystem()
512 self.__format_filesystem()
514 def mount(self, options = None):
516 DiskMount.mount(self, options)
519 msger.info("Checking filesystem %s" % self.disk.lofile)
520 runner.quiet(["/sbin/e2fsck", "-f", "-y", self.disk.lofile])
522 def __get_size_from_filesystem(self):
523 return int(self.__parse_field(runner.outs([self.dumpe2fs, '-h', self.disk.lofile]),
524 "Block count")) * self.blocksize
526 def __resize_to_minimal(self):
530 # Use a binary search to find the minimal size
531 # we can resize the image to
534 top = self.__get_size_from_filesystem()
535 while top != (bot + 1):
536 t = bot + ((top - bot) / 2)
538 if not resize2fs(self.disk.lofile, t):
544 def resparse(self, size = None):
549 minsize = self.__resize_to_minimal()
550 self.disk.truncate(minsize)
552 self.__resize_filesystem(size)
555 class VfatDiskMount(DiskMount):
556 """A DiskMount object that is able to format vfat/msdos filesystems."""
557 def __init__(self, disk, mountdir, fstype, blocksize, fslabel, rmmountdir=True, skipformat = False, fsopts = None):
558 DiskMount.__init__(self, disk, mountdir, fstype, rmmountdir)
559 self.blocksize = blocksize
560 self.fslabel = fslabel.replace("/", "")
561 self.uuid = "%08X" % int(time.time())
562 self.skipformat = skipformat
564 self.fsckcmd = find_binary_path("fsck." + self.fstype)
566 def __format_filesystem(self):
568 msger.debug("Skip filesystem format.")
571 msger.verbose("Formating %s filesystem on %s" % (self.fstype, self.disk.device))
572 rc = runner.show([self.mkfscmd, "-n", self.fslabel, "-i", self.uuid, self.disk.device])
574 raise MountError("Error creating %s filesystem on disk %s" % (self.fstype,self.disk.device))
576 msger.verbose("Tuning filesystem on %s" % self.disk.device)
578 def __resize_filesystem(self, size = None):
579 current_size = os.stat(self.disk.lofile)[stat.ST_SIZE]
582 size = self.disk.size
584 if size == current_size:
587 if size > current_size:
588 self.disk.expand(size)
592 #resize2fs(self.disk.lofile, size)
597 if not self.disk.fixed() and self.disk.exists():
603 self.__resize_filesystem()
605 self.__format_filesystem()
607 def mount(self, options = None):
609 DiskMount.mount(self, options)
612 msger.debug("Checking filesystem %s" % self.disk.lofile)
613 runner.show([self.fsckcmd, "-y", self.disk.lofile])
615 def __get_size_from_filesystem(self):
616 return self.disk.size
618 def __resize_to_minimal(self):
622 # Use a binary search to find the minimal size
623 # we can resize the image to
626 top = self.__get_size_from_filesystem()
629 def resparse(self, size = None):
631 minsize = self.__resize_to_minimal()
632 self.disk.truncate(minsize)
633 self.__resize_filesystem(size)
636 class BtrfsDiskMount(DiskMount):
637 """A DiskMount object that is able to format/resize btrfs filesystems."""
638 def __init__(self, disk, mountdir, fstype, blocksize, fslabel, rmmountdir=True, skipformat = False, fsopts = None):
640 DiskMount.__init__(self, disk, mountdir, fstype, rmmountdir)
641 self.blocksize = blocksize
642 self.fslabel = fslabel.replace("/", "")
644 self.skipformat = skipformat
646 self.blkidcmd = find_binary_path("blkid")
647 self.btrfsckcmd = find_binary_path("btrfsck")
649 def __check_btrfs(self):
651 """ Need to load btrfs module to mount it """
653 for line in open("/proc/filesystems").xreadlines():
654 if line.find("btrfs") > -1:
658 raise MountError("Your system can't mount btrfs filesystem, please make sure your kernel has btrfs support and the module btrfs.ko has been loaded.")
660 # disable selinux, selinux will block write
661 if os.path.exists("/usr/sbin/setenforce"):
662 runner.show(["/usr/sbin/setenforce", "0"])
664 def __parse_field(self, output, field):
665 for line in output.split(" "):
666 if line.startswith(field + "="):
667 return line[len(field) + 1:].strip().replace("\"", "")
669 raise KeyError("Failed to find field '%s' in output" % field)
671 def __format_filesystem(self):
673 msger.debug("Skip filesystem format.")
676 msger.verbose("Formating %s filesystem on %s" % (self.fstype, self.disk.device))
677 rc = runner.show([self.mkfscmd, "-L", self.fslabel, self.disk.device])
679 raise MountError("Error creating %s filesystem on disk %s" % (self.fstype,self.disk.device))
681 self.uuid = self.__parse_field(runner.outs([self.blkidcmd, self.disk.device]), "UUID")
683 def __resize_filesystem(self, size = None):
684 current_size = os.stat(self.disk.lofile)[stat.ST_SIZE]
687 size = self.disk.size
689 if size == current_size:
692 if size > current_size:
693 self.disk.expand(size)
700 if not self.disk.fixed() and self.disk.exists():
706 self.__resize_filesystem()
708 self.__format_filesystem()
710 def mount(self, options = None):
712 DiskMount.mount(self, options)
715 msger.debug("Checking filesystem %s" % self.disk.lofile)
716 runner.quiet([self.btrfsckcmd, self.disk.lofile])
718 def __get_size_from_filesystem(self):
719 return self.disk.size
721 def __resize_to_minimal(self):
724 return self.__get_size_from_filesystem()
726 def resparse(self, size = None):
728 minsize = self.__resize_to_minimal()
729 self.disk.truncate(minsize)
730 self.__resize_filesystem(size)
733 class DeviceMapperSnapshot(object):
734 def __init__(self, imgloop, cowloop):
735 self.imgloop = imgloop
736 self.cowloop = cowloop
738 self.__created = False
740 self.dmsetupcmd = find_binary_path("dmsetup")
742 """Load dm_snapshot if it isn't loaded"""
743 load_module("dm_snapshot")
746 if self.__name is None:
748 return os.path.join("/dev/mapper", self.__name)
749 path = property(get_path)
755 self.imgloop.create()
756 self.cowloop.create()
758 self.__name = "imgcreate-%d-%d" % (os.getpid(),
759 random.randint(0, 2**16))
761 size = os.stat(self.imgloop.lofile)[stat.ST_SIZE]
763 table = "0 %d snapshot %s %s p 8" % (size / 512,
767 args = [self.dmsetupcmd, "create", self.__name, "--table", table]
768 if runner.show(args) != 0:
769 self.cowloop.cleanup()
770 self.imgloop.cleanup()
771 raise SnapshotError("Could not create snapshot device using: " + ' '.join(args))
773 self.__created = True
775 def remove(self, ignore_errors = False):
776 if not self.__created:
780 rc = runner.show([self.dmsetupcmd, "remove", self.__name])
781 if not ignore_errors and rc != 0:
782 raise SnapshotError("Could not remove snapshot device")
785 self.__created = False
787 self.cowloop.cleanup()
788 self.imgloop.cleanup()
790 def get_cow_used(self):
791 if not self.__created:
795 # dmsetup status on a snapshot returns e.g.
796 # "0 8388608 snapshot 416/1048576"
797 # or, more generally:
799 # where C is the number of 512 byte sectors in use
801 out = runner.outs([self.dmsetupcmd, "status", self.__name])
803 return int((out.split()[3]).split('/')[0]) * 512
805 raise SnapshotError("Failed to parse dmsetup status: " + out)
807 def create_image_minimizer(path, image, minimal_size):
809 Builds a copy-on-write image which can be used to
810 create a device-mapper snapshot of an image where
811 the image's filesystem is as small as possible
814 1) Create a sparse COW
815 2) Loopback mount the image and the COW
816 3) Create a device-mapper snapshot of the image
818 4) Resize the filesystem to the minimal size
819 5) Determine the amount of space used in the COW
820 6) Restroy the device-mapper snapshot
821 7) Truncate the COW, removing unused space
822 8) Create a squashfs of the COW
824 imgloop = LoopbackDisk(image, None) # Passing bogus size - doesn't matter
826 cowloop = SparseLoopbackDisk(os.path.join(os.path.dirname(path), "osmin"),
829 snapshot = DeviceMapperSnapshot(imgloop, cowloop)
834 resize2fs(snapshot.path, minimal_size)
836 cow_used = snapshot.get_cow_used()
838 snapshot.remove(ignore_errors = (not sys.exc_info()[0] is None))
840 cowloop.truncate(cow_used)
842 mksquashfs(cowloop.lofile, path)
844 os.unlink(cowloop.lofile)
846 def load_module(module):
848 for line in open('/proc/modules').xreadlines():
849 if line.startswith("%s " % module):
853 msger.info("Loading %s..." % module)
854 runner.quiet(['modprobe', module])
856 class LoopDevice(object):
857 def __init__(self, loopid=None):
861 self.kpartxcmd = find_binary_path("kpartx")
862 self.losetupcmd = find_binary_path("losetup")
864 def register(self, device):
869 def reg_atexit(self):
871 atexit.register(self.close)
873 def _genloopid(self):
875 fint = lambda x: x[9:].isdigit() and int(x[9:]) or 0
876 maxid = 1 + max(filter(lambda x: x<100,
877 map(fint, glob.glob("/dev/loop[0-9]*"))))
878 if maxid < 10: maxid = 10
879 if maxid >= 100: raise
882 def _kpseek(self, device):
883 rc, out = runner.runtool([self.kpartxcmd, '-l', '-v', device])
885 raise MountError("Can't query dm snapshot on %s" % device)
886 for line in out.splitlines():
887 if line and line.startswith("loop"):
891 def _loseek(self, device):
893 rc, out = runner.runtool([self.losetupcmd, '-a'])
895 raise MountError("Failed to run 'losetup -a'")
896 for line in out.splitlines():
897 m = re.match("([^:]+): .*", line)
898 if m and m.group(1) == device:
905 self.loopid = self._genloopid()
906 self.device = "/dev/loop%d" % self.loopid
907 if os.path.exists(self.device):
908 if self._loseek(self.device):
909 raise MountError("Device busy: %s" % self.device)
914 mknod = find_binary_path('mknod')
915 rc = runner.show([mknod, '-m664', self.device, 'b', '7', str(self.loopid)])
917 raise MountError("Failed to create device %s" % self.device)
926 except MountError, e:
927 msger.error("%s" % e)
931 if self.device is None:
935 if self._kpseek(self.device):
937 for i in range(3, os.sysconf("SC_OPEN_MAX")):
942 runner.quiet([self.kpartxcmd, "-d", self.device])
943 if self._loseek(self.device):
944 runner.quiet([self.losetupcmd, "-d", self.device])
945 # FIXME: should sleep a while between two loseek
946 if self._loseek(self.device):
947 msger.warning("Can't cleanup loop device %s" % self.device)
949 os.unlink(self.device)
951 DEVICE_PIDFILE_DIR = "/var/tmp/mic/device"
953 def get_loop_device(losetupcmd, lofile):
955 fp = open("/var/lock/__mic_loopdev.lock", 'w')
956 fcntl.flock(fp, fcntl.LOCK_EX)
959 devinst = LoopDevice()
961 # clean up left loop device first
964 # provide an avaible loop device
965 rc, out = runner.runtool([losetupcmd, "--find"])
967 loopdev = out.split()[0]
968 devinst.register(loopdev)
969 if not loopdev or not os.path.exists(loopdev):
971 loopdev = devinst.device
973 # setup a loop device for image file
974 rc = runner.show([losetupcmd, loopdev, lofile])
976 raise MountError("Failed to setup loop device for '%s'" % lofile)
980 # try to save device and pid
981 makedirs(DEVICE_PIDFILE_DIR)
982 pidfile = os.path.join(DEVICE_PIDFILE_DIR, os.path.basename(loopdev))
983 if os.path.exists(pidfile):
985 with open(pidfile, 'w') as wf:
986 wf.write(str(os.getpid()))
988 except MountError, err:
989 raise CreatorError("%s" % str(err))
994 fcntl.flock(fp, fcntl.LOCK_UN)
996 os.unlink('/var/lock/__mic_loopdev.lock')
1002 def clean_loop_devices(piddir=DEVICE_PIDFILE_DIR):
1003 if not os.path.exists(piddir) or not os.path.isdir(piddir):
1006 for loopdev in os.listdir(piddir):
1007 pidfile = os.path.join(piddir, loopdev)
1009 with open(pidfile, 'r') as rf:
1010 devpid = int(rf.read())
1014 # if the process using this device is alive, skip it
1015 if not devpid or os.path.exists(os.path.join('/proc', str(devpid))):
1018 # try to clean it up
1020 devinst = LoopDevice()
1021 devinst.register(os.path.join('/dev', loopdev))