2 # fs.py : Filesystem related utilities and classes
4 # Copyright 2007, Red Hat Inc.
5 # Copyright 2009, 2010, 2011 Intel, Inc.
7 # This program is free software; you can redistribute it and/or modify
8 # it under the terms of the GNU General Public License as published by
9 # the Free Software Foundation; version 2 of the License.
11 # This program is distributed in the hope that it will be useful,
12 # but WITHOUT ANY WARRANTY; without even the implied warranty of
13 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 # GNU Library General Public License for more details.
16 # You should have received a copy of the GNU General Public License
17 # along with this program; if not, write to the Free Software
18 # Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
32 from urlgrabber.grabber import URLGrabber, URLGrabError
36 def terminal_width(fd=1):
37 """ Get the real terminal width """
40 buf = fcntl.ioctl(fd, termios.TIOCGWINSZ, buf)
41 return struct.unpack('hhhh', buf)[1]
45 def truncate_url(url, width):
46 return os.path.basename(url)[0:width]
48 class TextProgress(object):
49 def __init__(self, totalnum = None):
53 def start(self, filename, url, *args, **kwargs):
55 self.termwidth = terminal_width()
56 msger.info("\r%-*s" % (self.termwidth, " "))
57 if self.total is None:
58 msger.info("\rRetrieving %s ..." % truncate_url(self.url, self.termwidth - 15))
60 msger.info("\rRetrieving %s [%d/%d] ..." % (truncate_url(self.url, self.termwidth - 25), self.counter, self.total))
62 def update(self, *args):
66 if self.counter == self.total:
71 def find_binary_path(binary):
72 if os.environ.has_key("PATH"):
73 paths = os.environ["PATH"].split(":")
76 if os.environ.has_key("HOME"):
77 paths += [os.environ["HOME"] + "/bin"]
78 paths += ["/usr/local/sbin", "/usr/local/bin", "/usr/sbin", "/usr/bin", "/sbin", "/bin"]
81 bin_path = "%s/%s" % (path, binary)
82 if os.path.exists(bin_path):
84 raise CreatorError("Command '%s' is not available." % binary)
86 def makedirs(dirname):
87 """A version of os.makedirs() that doesn't throw an
88 exception if the leaf directory already exists.
92 except OSError, (err, msg):
93 if err != errno.EEXIST:
96 def mksquashfs(in_img, out_img):
97 fullpathmksquashfs = find_binary_path("mksquashfs")
98 args = [fullpathmksquashfs, in_img, out_img]
100 if not sys.stdout.isatty():
101 args.append("-no-progress")
103 ret = runner.show(args)
105 raise SquashfsError("'%s' exited with error (%d)" % (' '.join(args), ret))
107 def resize2fs(fs, size):
108 resize2fs = find_binary_path("resize2fs")
109 return runner.quiet([resize2fs, fs, "%sK" % (size / 1024,)])
112 fuser = find_binary_path("fuser")
113 if not os.path.exists(fp):
116 rc = runner.quiet([fuser, "-s", fp])
118 for pid in runner.outs([fuser, fp]).split():
119 fd = open("/proc/%s/cmdline" % pid, "r")
122 if cmdline[:-1] == "/bin/bash":
128 class BindChrootMount:
129 """Represents a bind mount of a directory into a chroot."""
130 def __init__(self, src, chroot, dest = None, option = None):
132 self.root = os.path.abspath(os.path.expanduser(chroot))
137 self.dest = self.root + "/" + dest
140 self.mountcmd = find_binary_path("mount")
141 self.umountcmd = find_binary_path("umount")
144 with open('/proc/mounts') as f:
146 if line.split()[1] == os.path.abspath(self.dest):
151 def has_chroot_instance(self):
152 lock = os.path.join(self.root, ".chroot.lock")
153 return my_fuser(lock)
156 if self.mounted or self.ismounted():
160 rc = runner.show([self.mountcmd, "--bind", self.src, self.dest])
162 raise MountError("Bind-mounting '%s' to '%s' failed" %
163 (self.src, self.dest))
165 rc = runner.show([self.mountcmd, "--bind", "-o", "remount,%s" % self.option, self.dest])
167 raise MountError("Bind-remounting '%s' failed" % self.dest)
171 if self.has_chroot_instance():
175 runner.show([self.umountcmd, "-l", self.dest])
179 """LoopbackMount compatibility layer for old API"""
180 def __init__(self, lofile, mountdir, fstype = None):
181 self.diskmount = DiskMount(LoopbackDisk(lofile,size = 0),mountdir,fstype,rmmountdir = True)
183 self.losetupcmd = find_binary_path("losetup")
186 self.diskmount.cleanup()
189 self.diskmount.unmount()
193 runner.show([self.losetupcmd, "-d", self.loopdev])
201 rc, losetupOutput = runner.runtool([self.losetupcmd, "-f"])
203 raise MountError("Failed to allocate loop device for '%s'" %
206 self.loopdev = losetupOutput.split()[0]
208 rc = runner.show([self.losetupcmd, self.loopdev, self.lofile])
210 raise MountError("Failed to allocate loop device for '%s'" %
216 self.diskmount.mount()
218 class SparseLoopbackMount(LoopbackMount):
219 """SparseLoopbackMount compatibility layer for old API"""
220 def __init__(self, lofile, mountdir, size, fstype = None):
221 self.diskmount = DiskMount(SparseLoopbackDisk(lofile,size),mountdir,fstype,rmmountdir = True)
223 def expand(self, create = False, size = None):
224 self.diskmount.disk.expand(create, size)
226 def truncate(self, size = None):
227 self.diskmount.disk.truncate(size)
230 self.diskmount.disk.create()
232 class SparseExtLoopbackMount(SparseLoopbackMount):
233 """SparseExtLoopbackMount compatibility layer for old API"""
234 def __init__(self, lofile, mountdir, size, fstype, blocksize, fslabel):
235 self.diskmount = ExtDiskMount(SparseLoopbackDisk(lofile,size), mountdir, fstype, blocksize, fslabel, rmmountdir = True)
238 def __format_filesystem(self):
239 self.diskmount.__format_filesystem()
242 self.diskmount.disk.create()
244 def resize(self, size = None):
245 return self.diskmount.__resize_filesystem(size)
248 self.diskmount.mount()
251 self.extdiskmount.__fsck()
253 def __get_size_from_filesystem(self):
254 return self.diskmount.__get_size_from_filesystem()
256 def __resize_to_minimal(self):
257 return self.diskmount.__resize_to_minimal()
259 def resparse(self, size = None):
260 return self.diskmount.resparse(size)
263 """Generic base object for a disk
265 The 'create' method must make the disk visible as a block device - eg
266 by calling losetup. For RawDisk, this is obviously a no-op. The 'cleanup'
267 method must undo the 'create' operation.
269 def __init__(self, size, device = None):
270 self._device = device
279 def get_device(self):
281 def set_device(self, path):
283 device = property(get_device, set_device)
287 size = property(get_size)
291 """A Disk backed by a block device.
292 Note that create() is a no-op.
294 def __init__(self, size, device):
295 Disk.__init__(self, size, device)
303 class LoopbackDisk(Disk):
304 """A Disk backed by a file via the loop module."""
305 def __init__(self, lofile, size):
306 Disk.__init__(self, size)
308 self.losetupcmd = find_binary_path("losetup")
314 return os.path.exists(self.lofile)
317 if self.device is not None:
320 rc, losetupOutput = runner.runtool([self.losetupcmd, "-f"])
322 raise MountError("Failed to allocate loop device for '%s'" %
325 device = losetupOutput.split()[0]
327 msger.debug("Losetup add %s mapping to %s" % (device, self.lofile))
328 rc = runner.show([self.losetupcmd, device, self.lofile])
330 raise MountError("Failed to allocate loop device for '%s'" %
335 if self.device is None:
337 msger.debug("Losetup remove %s" % self.device)
338 rc = runner.show([self.losetupcmd, "-d", self.device])
341 class SparseLoopbackDisk(LoopbackDisk):
342 """A Disk backed by a sparse file via the loop module."""
343 def __init__(self, lofile, size):
344 LoopbackDisk.__init__(self, lofile, size)
346 def expand(self, create = False, size = None):
350 if not os.path.exists(self.lofile):
351 makedirs(os.path.dirname(self.lofile))
356 msger.debug("Extending sparse file %s to %d" % (self.lofile, size))
358 fd = os.open(self.lofile, flags, 0644)
360 fd = os.open(self.lofile, flags)
362 os.lseek(fd, size, os.SEEK_SET)
366 def truncate(self, size = None):
370 msger.debug("Truncating sparse file %s to %d" % (self.lofile, size))
371 fd = os.open(self.lofile, os.O_WRONLY)
372 os.ftruncate(fd, size)
376 self.expand(create = True)
377 LoopbackDisk.create(self)
380 """A generic base class to deal with mounting things."""
381 def __init__(self, mountdir):
382 self.mountdir = mountdir
387 def mount(self, options = None):
393 class DiskMount(Mount):
394 """A Mount object that handles mounting of a Disk."""
395 def __init__(self, disk, mountdir, fstype = None, rmmountdir = True):
396 Mount.__init__(self, mountdir)
400 self.rmmountdir = rmmountdir
405 self.mkfscmd = find_binary_path("mkfs." + self.fstype)
408 self.mountcmd = find_binary_path("mount")
409 self.umountcmd = find_binary_path("umount")
417 msger.debug("Unmounting directory %s" % self.mountdir)
418 runner.quiet('sync') # sync the data on this mount point
419 rc = runner.show([self.umountcmd, "-l", self.mountdir])
423 raise MountError("Failed to umount %s" % self.mountdir)
424 if self.rmdir and not self.mounted:
426 os.rmdir(self.mountdir)
436 def mount(self, options = None):
440 if not os.path.isdir(self.mountdir):
441 msger.debug("Creating mount point %s" % self.mountdir)
442 os.makedirs(self.mountdir)
443 self.rmdir = self.rmmountdir
447 msger.debug("Mounting %s at %s" % (self.disk.device, self.mountdir))
449 args = [ self.mountcmd, "-o", options, self.disk.device, self.mountdir ]
451 args = [ self.mountcmd, self.disk.device, self.mountdir ]
453 args.extend(["-t", self.fstype])
455 rc = runner.show(args)
457 raise MountError("Failed to mount '%s' to '%s' with command '%s'. Retval: %s" %
458 (self.disk.device, self.mountdir, " ".join(args), rc))
462 class ExtDiskMount(DiskMount):
463 """A DiskMount object that is able to format/resize ext[23] filesystems."""
464 def __init__(self, disk, mountdir, fstype, blocksize, fslabel, rmmountdir=True, skipformat = False, fsopts = None):
465 DiskMount.__init__(self, disk, mountdir, fstype, rmmountdir)
466 self.blocksize = blocksize
467 self.fslabel = fslabel.replace("/", "")
469 self.skipformat = skipformat
471 self.dumpe2fs = find_binary_path("dumpe2fs")
472 self.tune2fs = find_binary_path("tune2fs")
474 def __parse_field(self, output, field):
475 for line in output.split("\n"):
476 if line.startswith(field + ":"):
477 return line[len(field) + 1:].strip()
479 raise KeyError("Failed to find field '%s' in output" % field)
481 def __format_filesystem(self):
483 msger.debug("Skip filesystem format.")
486 msger.verbose("Formating %s filesystem on %s" % (self.fstype, self.disk.device))
487 rc = runner.show([self.mkfscmd,
488 "-F", "-L", self.fslabel,
489 "-m", "1", "-b", str(self.blocksize),
490 self.disk.device]) # str(self.disk.size / self.blocksize)])
492 raise MountError("Error creating %s filesystem on disk %s" % (self.fstype, self.disk.device))
494 out = runner.outs([self.dumpe2fs, '-h', self.disk.device])
496 self.uuid = self.__parse_field(out, "Filesystem UUID")
497 msger.debug("Tuning filesystem on %s" % self.disk.device)
498 runner.show([self.tune2fs, "-c0", "-i0", "-Odir_index", "-ouser_xattr,acl", self.disk.device])
500 def __resize_filesystem(self, size = None):
501 current_size = os.stat(self.disk.lofile)[stat.ST_SIZE]
504 size = self.disk.size
506 if size == current_size:
509 if size > current_size:
510 self.disk.expand(size)
514 resize2fs(self.disk.lofile, size)
519 if not self.disk.fixed() and self.disk.exists():
525 self.__resize_filesystem()
527 self.__format_filesystem()
529 def mount(self, options = None):
531 DiskMount.mount(self, options)
534 msger.info("Checking filesystem %s" % self.disk.lofile)
535 runner.show(["/sbin/e2fsck", "-f", "-y", self.disk.lofile])
537 def __get_size_from_filesystem(self):
538 return int(self.__parse_field(runner.outs([self.dumpe2fs, '-h', self.disk.lofile]),
539 "Block count")) * self.blocksize
541 def __resize_to_minimal(self):
545 # Use a binary search to find the minimal size
546 # we can resize the image to
549 top = self.__get_size_from_filesystem()
550 while top != (bot + 1):
551 t = bot + ((top - bot) / 2)
553 if not resize2fs(self.disk.lofile, t):
559 def resparse(self, size = None):
561 minsize = self.__resize_to_minimal()
562 self.disk.truncate(minsize)
563 self.__resize_filesystem(size)
566 class VfatDiskMount(DiskMount):
567 """A DiskMount object that is able to format vfat/msdos filesystems."""
568 def __init__(self, disk, mountdir, fstype, blocksize, fslabel, rmmountdir=True, skipformat = False, fsopts = None):
569 DiskMount.__init__(self, disk, mountdir, fstype, rmmountdir)
570 self.blocksize = blocksize
571 self.fslabel = fslabel.replace("/", "")
572 self.uuid = "%08X" % int(time.time())
573 self.skipformat = skipformat
575 self.fsckcmd = find_binary_path("fsck." + self.fstype)
577 def __format_filesystem(self):
579 msger.debug("Skip filesystem format.")
582 msger.verbose("Formating %s filesystem on %s" % (self.fstype, self.disk.device))
583 rc = runner.show([self.mkfscmd, "-n", self.fslabel, "-i", self.uuid, self.disk.device])
585 raise MountError("Error creating %s filesystem on disk %s" % (self.fstype,self.disk.device))
587 msger.verbose("Tuning filesystem on %s" % self.disk.device)
589 def __resize_filesystem(self, size = None):
590 current_size = os.stat(self.disk.lofile)[stat.ST_SIZE]
593 size = self.disk.size
595 if size == current_size:
598 if size > current_size:
599 self.disk.expand(size)
603 #resize2fs(self.disk.lofile, size)
608 if not self.disk.fixed() and self.disk.exists():
614 self.__resize_filesystem()
616 self.__format_filesystem()
618 def mount(self, options = None):
620 DiskMount.mount(self, options)
623 msger.debug("Checking filesystem %s" % self.disk.lofile)
624 runner.show([self.fsckcmd, "-y", self.disk.lofile])
626 def __get_size_from_filesystem(self):
627 return self.disk.size
629 def __resize_to_minimal(self):
633 # Use a binary search to find the minimal size
634 # we can resize the image to
637 top = self.__get_size_from_filesystem()
640 def resparse(self, size = None):
642 minsize = self.__resize_to_minimal()
643 self.disk.truncate(minsize)
644 self.__resize_filesystem(size)
647 class BtrfsDiskMount(DiskMount):
648 """A DiskMount object that is able to format/resize btrfs filesystems."""
649 def __init__(self, disk, mountdir, fstype, blocksize, fslabel, rmmountdir=True, skipformat = False, fsopts = None):
651 DiskMount.__init__(self, disk, mountdir, fstype, rmmountdir)
652 self.blocksize = blocksize
653 self.fslabel = fslabel.replace("/", "")
655 self.skipformat = skipformat
657 self.blkidcmd = find_binary_path("blkid")
658 self.btrfsckcmd = find_binary_path("btrfsck")
660 def __check_btrfs(self):
662 """ Need to load btrfs module to mount it """
664 for line in open("/proc/filesystems").xreadlines():
665 if line.find("btrfs") > -1:
669 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.")
671 # disable selinux, selinux will block write
672 if os.path.exists("/usr/sbin/setenforce"):
673 runner.show(["/usr/sbin/setenforce", "0"])
675 def __parse_field(self, output, field):
676 for line in output.split(" "):
677 if line.startswith(field + "="):
678 return line[len(field) + 1:].strip().replace("\"", "")
680 raise KeyError("Failed to find field '%s' in output" % field)
682 def __format_filesystem(self):
684 msger.debug("Skip filesystem format.")
687 msger.verbose("Formating %s filesystem on %s" % (self.fstype, self.disk.device))
688 rc = runner.show([self.mkfscmd, "-L", self.fslabel, self.disk.device])
690 raise MountError("Error creating %s filesystem on disk %s" % (self.fstype,self.disk.device))
692 self.uuid = self.__parse_field(runner.outs([self.blkidcmd, self.disk.device]), "UUID")
694 def __resize_filesystem(self, size = None):
695 current_size = os.stat(self.disk.lofile)[stat.ST_SIZE]
698 size = self.disk.size
700 if size == current_size:
703 if size > current_size:
704 self.disk.expand(size)
711 if not self.disk.fixed() and self.disk.exists():
717 self.__resize_filesystem()
719 self.__format_filesystem()
721 def mount(self, options = None):
723 DiskMount.mount(self, options)
726 msger.debug("Checking filesystem %s" % self.disk.lofile)
727 runner.show([self.btrfsckcmd, self.disk.lofile])
729 def __get_size_from_filesystem(self):
730 return self.disk.size
732 def __resize_to_minimal(self):
735 return self.__get_size_from_filesystem()
737 def resparse(self, size = None):
739 minsize = self.__resize_to_minimal()
740 self.disk.truncate(minsize)
741 self.__resize_filesystem(size)
744 class DeviceMapperSnapshot(object):
745 def __init__(self, imgloop, cowloop):
746 self.imgloop = imgloop
747 self.cowloop = cowloop
749 self.__created = False
751 self.dmsetupcmd = find_binary_path("dmsetup")
753 """Load dm_snapshot if it isn't loaded"""
754 load_module("dm_snapshot")
757 if self.__name is None:
759 return os.path.join("/dev/mapper", self.__name)
760 path = property(get_path)
766 self.imgloop.create()
767 self.cowloop.create()
769 self.__name = "imgcreate-%d-%d" % (os.getpid(),
770 random.randint(0, 2**16))
772 size = os.stat(self.imgloop.lofile)[stat.ST_SIZE]
774 table = "0 %d snapshot %s %s p 8" % (size / 512,
778 args = [self.dmsetupcmd, "create", self.__name, "--table", table]
779 if runner.show(args) != 0:
780 self.cowloop.cleanup()
781 self.imgloop.cleanup()
782 raise SnapshotError("Could not create snapshot device using: " + ' '.join(args))
784 self.__created = True
786 def remove(self, ignore_errors = False):
787 if not self.__created:
791 rc = runner.show([self.dmsetupcmd, "remove", self.__name])
792 if not ignore_errors and rc != 0:
793 raise SnapshotError("Could not remove snapshot device")
796 self.__created = False
798 self.cowloop.cleanup()
799 self.imgloop.cleanup()
801 def get_cow_used(self):
802 if not self.__created:
806 # dmsetup status on a snapshot returns e.g.
807 # "0 8388608 snapshot 416/1048576"
808 # or, more generally:
810 # where C is the number of 512 byte sectors in use
812 out = runner.outs([self.dmsetupcmd, "status", self.__name])
814 return int((out.split()[3]).split('/')[0]) * 512
816 raise SnapshotError("Failed to parse dmsetup status: " + out)
818 def create_image_minimizer(path, image, minimal_size):
820 Builds a copy-on-write image which can be used to
821 create a device-mapper snapshot of an image where
822 the image's filesystem is as small as possible
825 1) Create a sparse COW
826 2) Loopback mount the image and the COW
827 3) Create a device-mapper snapshot of the image
829 4) Resize the filesystem to the minimal size
830 5) Determine the amount of space used in the COW
831 6) Restroy the device-mapper snapshot
832 7) Truncate the COW, removing unused space
833 8) Create a squashfs of the COW
835 imgloop = LoopbackDisk(image, None) # Passing bogus size - doesn't matter
837 cowloop = SparseLoopbackDisk(os.path.join(os.path.dirname(path), "osmin"),
840 snapshot = DeviceMapperSnapshot(imgloop, cowloop)
845 resize2fs(snapshot.path, minimal_size)
847 cow_used = snapshot.get_cow_used()
849 snapshot.remove(ignore_errors = (not sys.exc_info()[0] is None))
851 cowloop.truncate(cow_used)
853 mksquashfs(cowloop.lofile, path)
855 os.unlink(cowloop.lofile)
857 def load_module(module):
859 for line in open('/proc/modules').xreadlines():
860 if line.startswith("%s " % module):
864 msger.info("Loading %s..." % module)
865 runner.quiet(['modprobe', module])
867 def myurlgrab(url, filename, proxies, progress_obj = None):
869 if progress_obj is None:
870 progress_obj = TextProgress()
872 if url.startswith("file:///"):
873 file = url.replace("file://", "")
874 if not os.path.exists(file):
875 raise CreatorError("URLGrabber error: can't find file %s" % file)
876 runner.show(['cp', "-f", file, filename])
879 filename = g.urlgrab(url = url, filename = filename,
880 ssl_verify_host = False, ssl_verify_peer = False,
881 proxies = proxies, http_headers = (('Pragma', 'no-cache'),), progress_obj = progress_obj)
882 except URLGrabError, e:
883 raise CreatorError("URLGrabber error: %s" % url)