2 # fs.py : Filesystem related utilities and classes
4 # Copyright 2007, Red Hat Inc.
6 # This program is free software; you can redistribute it and/or modify
7 # it under the terms of the GNU General Public License as published by
8 # the Free Software Foundation; version 2 of the License.
10 # This program is distributed in the hope that it will be useful,
11 # but WITHOUT ANY WARRANTY; without even the implied warranty of
12 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 # GNU Library General Public License for more details.
15 # You should have received a copy of the GNU General Public License
16 # along with this program; if not, write to the Free Software
17 # Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
34 from urlgrabber.grabber import URLGrabber
35 from urlgrabber.grabber import URLGrabError
37 def terminal_width(fd=1):
38 """ Get the real terminal width """
41 buf = fcntl.ioctl(fd, termios.TIOCGWINSZ, buf)
42 return struct.unpack('hhhh', buf)[1]
46 def truncate_url(url, width):
48 return os.path.basename(url)[0:width]
51 class TextProgress(object):
52 def start(self, filename, url, *args, **kwargs):
54 self.termwidth = terminal_width()
55 sys.stdout.write("Retrieving %s " % truncate_url(self.url, self.termwidth - 17))
57 self.indicators = ["-", "\\", "|", "/"]
59 def update(self, *args):
60 if sys.stdout.isatty():
61 sys.stdout.write("\rRetrieving %s %s" % (truncate_url(self.url, self.termwidth - 17), self.indicators[self.counter%4]))
67 if sys.stdout.isatty():
68 sys.stdout.write("\rRetrieving %s ...OK\n" % (self.url,))
70 sys.stdout.write("...OK\n")
73 def find_binary_path(binary):
74 if os.environ.has_key("PATH"):
75 paths = os.environ["PATH"].split(":")
78 if os.environ.has_key("HOME"):
79 paths += [os.environ["HOME"] + "/bin"]
80 paths += ["/usr/local/sbin", "/usr/local/bin", "/usr/sbin", "/usr/bin", "/sbin", "/bin"]
83 bin_path = "%s/%s" % (path, binary)
84 if os.path.exists(bin_path):
86 raise CreatorError("Command '%s' is not available." % binary)
88 def makedirs(dirname):
89 """A version of os.makedirs() that doesn't throw an
90 exception if the leaf directory already exists.
94 except OSError, (err, msg):
95 if err != errno.EEXIST:
98 def mksquashfs(in_img, out_img):
99 fullpathmksquashfs = find_binary_path("mksquashfs")
100 args = [fullpathmksquashfs, in_img, out_img]
102 if not sys.stdout.isatty():
103 args.append("-no-progress")
105 ret = subprocess.call(args, stdout=sys.stdout, stderr=sys.stderr)
107 raise SquashfsError("'%s' exited with error (%d)" %
108 (string.join(args, " "), ret))
110 def resize2fs(fs, size):
111 dev_null = os.open("/dev/null", os.O_WRONLY)
113 resize2fs = find_binary_path("resize2fs")
114 return subprocess.call([resize2fs, fs, "%sK" % (size / 1024,)],
115 stdout = dev_null, stderr = dev_null)
121 fuser = find_binary_path("fuser")
122 if not os.path.exists(file):
124 dev_null = os.open("/dev/null", os.O_WRONLY)
125 rc = subprocess.call([fuser, "-s", file], stderr=dev_null)
127 fuser_proc = subprocess.Popen([fuser, file], stdout=subprocess.PIPE, stderr=dev_null)
128 pids = fuser_proc.communicate()[0].strip().split()
130 fd = open("/proc/%s/cmdline" % pid, "r")
133 if cmdline[:-1] == "/bin/bash":
139 class BindChrootMount:
140 """Represents a bind mount of a directory into a chroot."""
141 def __init__(self, src, chroot, dest = None, option = None):
143 self.root = os.path.abspath(os.path.expanduser(chroot))
148 self.dest = self.root + "/" + dest
151 self.mountcmd = find_binary_path("mount")
152 self.umountcmd = find_binary_path("umount")
156 dev_null = os.open("/dev/null", os.O_WRONLY)
157 catcmd = find_binary_path("cat")
158 args = [ catcmd, "/proc/mounts" ]
159 proc_mounts = subprocess.Popen(args, stdout=subprocess.PIPE, stderr=dev_null)
160 outputs = proc_mounts.communicate()[0].strip().split("\n")
162 if line.split()[1] == os.path.abspath(self.dest):
168 def has_chroot_instance(self):
169 lock = os.path.join(self.root, ".chroot.lock")
170 return my_fuser(lock)
173 if self.mounted or self.ismounted():
177 rc = subprocess.call([self.mountcmd, "--bind", self.src, self.dest])
179 raise MountError("Bind-mounting '%s' to '%s' failed" %
180 (self.src, self.dest))
182 rc = subprocess.call([self.mountcmd, "-o", "remount,%s" % self.option, self.dest])
184 raise MountError("Bind-remounting '%s' failed" % self.dest)
188 if self.has_chroot_instance():
192 subprocess.call([self.umountcmd, "-l", self.dest])
196 """LoopbackMount compatibility layer for old API"""
197 def __init__(self, lofile, mountdir, fstype = None):
198 self.diskmount = DiskMount(LoopbackDisk(lofile,size = 0),mountdir,fstype,rmmountdir = True)
200 self.losetupcmd = find_binary_path("losetup")
203 self.diskmount.cleanup()
206 self.diskmount.unmount()
210 rc = subprocess.call([self.losetupcmd, "-d", self.loopdev])
218 losetupProc = subprocess.Popen([self.losetupcmd, "-f"],
219 stdout=subprocess.PIPE)
220 losetupOutput = losetupProc.communicate()[0]
222 if losetupProc.returncode:
223 raise MountError("Failed to allocate loop device for '%s'" %
226 self.loopdev = losetupOutput.split()[0]
228 rc = subprocess.call([self.losetupcmd, self.loopdev, self.lofile])
230 raise MountError("Failed to allocate loop device for '%s'" %
236 self.diskmount.mount()
238 class SparseLoopbackMount(LoopbackMount):
239 """SparseLoopbackMount compatibility layer for old API"""
240 def __init__(self, lofile, mountdir, size, fstype = None):
241 self.diskmount = DiskMount(SparseLoopbackDisk(lofile,size),mountdir,fstype,rmmountdir = True)
243 def expand(self, create = False, size = None):
244 self.diskmount.disk.expand(create, size)
246 def truncate(self, size = None):
247 self.diskmount.disk.truncate(size)
250 self.diskmount.disk.create()
252 class SparseExtLoopbackMount(SparseLoopbackMount):
253 """SparseExtLoopbackMount compatibility layer for old API"""
254 def __init__(self, lofile, mountdir, size, fstype, blocksize, fslabel):
255 self.diskmount = ExtDiskMount(SparseLoopbackDisk(lofile,size), mountdir, fstype, blocksize, fslabel, rmmountdir = True)
258 def __format_filesystem(self):
259 self.diskmount.__format_filesystem()
262 self.diskmount.disk.create()
264 def resize(self, size = None):
265 return self.diskmount.__resize_filesystem(size)
268 self.diskmount.mount()
271 self.extdiskmount.__fsck()
273 def __get_size_from_filesystem(self):
274 return self.diskmount.__get_size_from_filesystem()
276 def __resize_to_minimal(self):
277 return self.diskmount.__resize_to_minimal()
279 def resparse(self, size = None):
280 return self.diskmount.resparse(size)
283 """Generic base object for a disk
285 The 'create' method must make the disk visible as a block device - eg
286 by calling losetup. For RawDisk, this is obviously a no-op. The 'cleanup'
287 method must undo the 'create' operation.
289 def __init__(self, size, device = None):
290 self._device = device
299 def get_device(self):
301 def set_device(self, path):
303 device = property(get_device, set_device)
307 size = property(get_size)
311 """A Disk backed by a block device.
312 Note that create() is a no-op.
314 def __init__(self, size, device):
315 Disk.__init__(self, size, device)
323 class LoopbackDisk(Disk):
324 """A Disk backed by a file via the loop module."""
325 def __init__(self, lofile, size):
326 Disk.__init__(self, size)
328 self.losetupcmd = find_binary_path("losetup")
334 return os.path.exists(self.lofile)
337 if self.device is not None:
340 losetupProc = subprocess.Popen([self.losetupcmd, "-f"],
341 stdout=subprocess.PIPE)
342 losetupOutput = losetupProc.communicate()[0]
344 if losetupProc.returncode:
345 raise MountError("Failed to allocate loop device for '%s'" %
348 device = losetupOutput.split()[0]
350 logging.debug("Losetup add %s mapping to %s" % (device, self.lofile))
351 rc = subprocess.call([self.losetupcmd, device, self.lofile])
353 raise MountError("Failed to allocate loop device for '%s'" %
358 if self.device is None:
360 logging.debug("Losetup remove %s" % self.device)
361 rc = subprocess.call([self.losetupcmd, "-d", self.device])
366 class SparseLoopbackDisk(LoopbackDisk):
367 """A Disk backed by a sparse file via the loop module."""
368 def __init__(self, lofile, size):
369 LoopbackDisk.__init__(self, lofile, size)
371 def expand(self, create = False, size = None):
375 if not os.path.exists(self.lofile):
376 makedirs(os.path.dirname(self.lofile))
381 logging.debug("Extending sparse file %s to %d" % (self.lofile, size))
383 fd = os.open(self.lofile, flags, 0644)
385 fd = os.open(self.lofile, flags)
387 os.lseek(fd, size, os.SEEK_SET)
391 def truncate(self, size = None):
395 logging.debug("Truncating sparse file %s to %d" % (self.lofile, size))
396 fd = os.open(self.lofile, os.O_WRONLY)
397 os.ftruncate(fd, size)
401 self.expand(create = True)
402 LoopbackDisk.create(self)
405 """A generic base class to deal with mounting things."""
406 def __init__(self, mountdir):
407 self.mountdir = mountdir
412 def mount(self, options = None):
418 class DiskMount(Mount):
419 """A Mount object that handles mounting of a Disk."""
420 def __init__(self, disk, mountdir, fstype = None, rmmountdir = True):
421 Mount.__init__(self, mountdir)
425 self.rmmountdir = rmmountdir
430 self.mkfscmd = find_binary_path("mkfs." + self.fstype)
433 self.mountcmd = find_binary_path("mount")
434 self.umountcmd = find_binary_path("umount")
442 logging.debug("Unmounting directory %s" % self.mountdir)
443 synccmd = find_binary_path("sync")
444 subprocess.call([synccmd]) # sync the data on this mount point
445 rc = subprocess.call([self.umountcmd, "-l", self.mountdir])
449 raise MountError("Failed to umount %s" % self.mountdir)
450 if self.rmdir and not self.mounted:
452 os.rmdir(self.mountdir)
462 def mount(self, options = None):
466 if not os.path.isdir(self.mountdir):
467 logging.debug("Creating mount point %s" % self.mountdir)
468 os.makedirs(self.mountdir)
469 self.rmdir = self.rmmountdir
473 logging.debug("Mounting %s at %s" % (self.disk.device, self.mountdir))
475 args = [ self.mountcmd, "-o", options, self.disk.device, self.mountdir ]
477 args = [ self.mountcmd, self.disk.device, self.mountdir ]
479 args.extend(["-t", self.fstype])
481 rc = subprocess.call(args)
483 raise MountError("Failed to mount '%s' to '%s' with command '%s'. Retval: %s" %
484 (self.disk.device, self.mountdir, " ".join(args), rc))
488 class ExtDiskMount(DiskMount):
489 """A DiskMount object that is able to format/resize ext[23] filesystems."""
490 def __init__(self, disk, mountdir, fstype, blocksize, fslabel, rmmountdir=True, skipformat = False, fsopts = None):
491 DiskMount.__init__(self, disk, mountdir, fstype, rmmountdir)
492 self.blocksize = blocksize
493 self.fslabel = fslabel.replace("/", "")
495 self.skipformat = skipformat
497 self.dumpe2fs = find_binary_path("dumpe2fs")
498 self.tune2fs = find_binary_path("tune2fs")
500 def __parse_field(self, output, field):
501 for line in output.split("\n"):
502 if line.startswith(field + ":"):
503 return line[len(field) + 1:].strip()
505 raise KeyError("Failed to find field '%s' in output" % field)
507 def __format_filesystem(self):
509 logging.debug("Skip filesystem format.")
511 logging.debug("Formating %s filesystem on %s" % (self.fstype, self.disk.device))
512 rc = subprocess.call([self.mkfscmd,
513 "-F", "-L", self.fslabel,
514 "-m", "1", "-b", str(self.blocksize),
515 self.disk.device], stdout=sys.stdout,
517 # str(self.disk.size / self.blocksize)])
519 raise MountError("Error creating %s filesystem on disk %s" % (self.fstype,self.disk.device))
521 dev_null = os.open("/dev/null", os.O_WRONLY)
523 out = subprocess.Popen([self.dumpe2fs, '-h', self.disk.device],
524 stdout = subprocess.PIPE,
525 stderr = dev_null).communicate()[0]
529 self.uuid = self.__parse_field(out, "Filesystem UUID")
530 logging.debug("Tuning filesystem on %s" % self.disk.device)
531 subprocess.call([self.tune2fs, "-c0", "-i0", "-Odir_index",
532 "-ouser_xattr,acl", self.disk.device],
533 stdout=sys.stdout, stderr=sys.stderr)
535 def __resize_filesystem(self, size = None):
536 current_size = os.stat(self.disk.lofile)[stat.ST_SIZE]
539 size = self.disk.size
541 if size == current_size:
544 if size > current_size:
545 self.disk.expand(size)
549 resize2fs(self.disk.lofile, size)
554 if not self.disk.fixed() and self.disk.exists():
560 self.__resize_filesystem()
562 self.__format_filesystem()
564 def mount(self, options = None):
566 DiskMount.mount(self, options)
569 logging.debug("Checking filesystem %s" % self.disk.lofile)
570 subprocess.call(["/sbin/e2fsck", "-f", "-y", self.disk.lofile], stdout=sys.stdout, stderr=sys.stderr)
572 def __get_size_from_filesystem(self):
573 dev_null = os.open("/dev/null", os.O_WRONLY)
575 out = subprocess.Popen([self.dumpe2fs, '-h', self.disk.lofile],
576 stdout = subprocess.PIPE,
577 stderr = dev_null).communicate()[0]
581 return int(self.__parse_field(out, "Block count")) * self.blocksize
583 def __resize_to_minimal(self):
587 # Use a binary search to find the minimal size
588 # we can resize the image to
591 top = self.__get_size_from_filesystem()
592 while top != (bot + 1):
593 t = bot + ((top - bot) / 2)
595 if not resize2fs(self.disk.lofile, t):
601 def resparse(self, size = None):
603 minsize = self.__resize_to_minimal()
604 self.disk.truncate(minsize)
605 self.__resize_filesystem(size)
608 class VfatDiskMount(DiskMount):
609 """A DiskMount object that is able to format vfat/msdos filesystems."""
610 def __init__(self, disk, mountdir, fstype, blocksize, fslabel, rmmountdir=True, skipformat = False, fsopts = None):
611 DiskMount.__init__(self, disk, mountdir, fstype, rmmountdir)
612 self.blocksize = blocksize
613 self.fslabel = fslabel.replace("/", "")
614 self.uuid = "%08X" % int(time.time())
615 self.skipformat = skipformat
617 self.fsckcmd = find_binary_path("fsck." + self.fstype)
619 def __format_filesystem(self):
621 logging.debug("Skip filesystem format.")
623 logging.debug("Formating %s filesystem on %s" % (self.fstype, self.disk.device))
624 blah = [self.mkfscmd, "-n", self.fslabel, "-i", self.uuid, self.disk.device]
625 rc = subprocess.call(blah)
627 raise MountError("Error creating %s filesystem on disk %s" % (self.fstype,self.disk.device))
628 logging.debug("Tuning filesystem on %s" % self.disk.device)
630 def __resize_filesystem(self, size = None):
631 current_size = os.stat(self.disk.lofile)[stat.ST_SIZE]
634 size = self.disk.size
636 if size == current_size:
639 if size > current_size:
640 self.disk.expand(size)
644 #resize2fs(self.disk.lofile, size)
649 if not self.disk.fixed() and self.disk.exists():
655 self.__resize_filesystem()
657 self.__format_filesystem()
659 def mount(self, options = None):
661 DiskMount.mount(self, options)
664 logging.debug("Checking filesystem %s" % self.disk.lofile)
665 subprocess.call([self.fsckcmd, "-y", self.disk.lofile])
667 def __get_size_from_filesystem(self):
668 return self.disk.size
670 def __resize_to_minimal(self):
674 # Use a binary search to find the minimal size
675 # we can resize the image to
678 top = self.__get_size_from_filesystem()
681 def resparse(self, size = None):
683 minsize = self.__resize_to_minimal()
684 self.disk.truncate(minsize)
685 self.__resize_filesystem(size)
688 class BtrfsDiskMount(DiskMount):
689 """A DiskMount object that is able to format/resize btrfs filesystems."""
690 def __init__(self, disk, mountdir, fstype, blocksize, fslabel, rmmountdir=True, skipformat = False, fsopts = None):
692 DiskMount.__init__(self, disk, mountdir, fstype, rmmountdir)
693 self.blocksize = blocksize
694 self.fslabel = fslabel.replace("/", "")
696 self.skipformat = skipformat
698 self.blkidcmd = find_binary_path("blkid")
699 self.btrfsckcmd = find_binary_path("btrfsck")
701 def __check_btrfs(self):
703 """ Need to load btrfs module to mount it """
705 for line in open("/proc/filesystems").xreadlines():
706 if line.find("btrfs") > -1:
710 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.")
712 # disable selinux, selinux will block write
713 if os.path.exists("/usr/sbin/setenforce"):
714 subprocess.call(["/usr/sbin/setenforce", "0"])
716 def __parse_field(self, output, field):
717 for line in output.split(" "):
718 if line.startswith(field + "="):
719 return line[len(field) + 1:].strip().replace("\"", "")
721 raise KeyError("Failed to find field '%s' in output" % field)
723 def __format_filesystem(self):
725 logging.debug("Skip filesystem format.")
727 logging.debug("Formating %s filesystem on %s" % (self.fstype, self.disk.device))
728 rc = subprocess.call([self.mkfscmd, "-L", self.fslabel, self.disk.device])
730 raise MountError("Error creating %s filesystem on disk %s" % (self.fstype,self.disk.device))
732 dev_null = os.open("/dev/null", os.O_WRONLY)
734 out = subprocess.Popen([self.blkidcmd, self.disk.device],
735 stdout = subprocess.PIPE,
736 stderr = dev_null).communicate()[0]
740 self.uuid = self.__parse_field(out, "UUID")
742 def __resize_filesystem(self, size = None):
743 current_size = os.stat(self.disk.lofile)[stat.ST_SIZE]
746 size = self.disk.size
748 if size == current_size:
751 if size > current_size:
752 self.disk.expand(size)
759 if not self.disk.fixed() and self.disk.exists():
765 self.__resize_filesystem()
767 self.__format_filesystem()
769 def mount(self, options = None):
771 DiskMount.mount(self, options)
774 logging.debug("Checking filesystem %s" % self.disk.lofile)
775 subprocess.call([self.btrfsckcmd, self.disk.lofile])
777 def __get_size_from_filesystem(self):
778 return self.disk.size
780 def __resize_to_minimal(self):
783 return self.__get_size_from_filesystem()
785 def resparse(self, size = None):
787 minsize = self.__resize_to_minimal()
788 self.disk.truncate(minsize)
789 self.__resize_filesystem(size)
792 class DeviceMapperSnapshot(object):
793 def __init__(self, imgloop, cowloop):
794 self.imgloop = imgloop
795 self.cowloop = cowloop
797 self.__created = False
799 self.dmsetupcmd = find_binary_path("dmsetup")
801 """Load dm_snapshot if it isn't loaded"""
802 load_module("dm_snapshot")
805 if self.__name is None:
807 return os.path.join("/dev/mapper", self.__name)
808 path = property(get_path)
814 self.imgloop.create()
815 self.cowloop.create()
817 self.__name = "imgcreate-%d-%d" % (os.getpid(),
818 random.randint(0, 2**16))
820 size = os.stat(self.imgloop.lofile)[stat.ST_SIZE]
822 table = "0 %d snapshot %s %s p 8" % (size / 512,
826 args = [self.dmsetupcmd, "create", self.__name, "--table", table]
827 if subprocess.call(args) != 0:
828 self.cowloop.cleanup()
829 self.imgloop.cleanup()
830 raise SnapshotError("Could not create snapshot device using: " +
831 string.join(args, " "))
833 self.__created = True
835 def remove(self, ignore_errors = False):
836 if not self.__created:
840 rc = subprocess.call([self.dmsetupcmd, "remove", self.__name])
841 if not ignore_errors and rc != 0:
842 raise SnapshotError("Could not remove snapshot device")
845 self.__created = False
847 self.cowloop.cleanup()
848 self.imgloop.cleanup()
850 def get_cow_used(self):
851 if not self.__created:
854 dev_null = os.open("/dev/null", os.O_WRONLY)
856 out = subprocess.Popen([self.dmsetupcmd, "status", self.__name],
857 stdout = subprocess.PIPE,
858 stderr = dev_null).communicate()[0]
863 # dmsetup status on a snapshot returns e.g.
864 # "0 8388608 snapshot 416/1048576"
865 # or, more generally:
867 # where C is the number of 512 byte sectors in use
870 return int((out.split()[3]).split('/')[0]) * 512
872 raise SnapshotError("Failed to parse dmsetup status: " + out)
874 def create_image_minimizer(path, image, minimal_size):
876 Builds a copy-on-write image which can be used to
877 create a device-mapper snapshot of an image where
878 the image's filesystem is as small as possible
881 1) Create a sparse COW
882 2) Loopback mount the image and the COW
883 3) Create a device-mapper snapshot of the image
885 4) Resize the filesystem to the minimal size
886 5) Determine the amount of space used in the COW
887 6) Restroy the device-mapper snapshot
888 7) Truncate the COW, removing unused space
889 8) Create a squashfs of the COW
891 imgloop = LoopbackDisk(image, None) # Passing bogus size - doesn't matter
893 cowloop = SparseLoopbackDisk(os.path.join(os.path.dirname(path), "osmin"),
896 snapshot = DeviceMapperSnapshot(imgloop, cowloop)
901 resize2fs(snapshot.path, minimal_size)
903 cow_used = snapshot.get_cow_used()
905 snapshot.remove(ignore_errors = (not sys.exc_info()[0] is None))
907 cowloop.truncate(cow_used)
909 mksquashfs(cowloop.lofile, path)
911 os.unlink(cowloop.lofile)
913 def load_module(module):
915 for line in open('/proc/modules').xreadlines():
916 if line.startswith("%s " % module):
920 print "Loading %s..." % module
921 dev_null = os.open("/dev/null", os.O_WRONLY)
922 modprobecmd = find_binary_path("modprobe")
923 modprobe = subprocess.Popen([modprobecmd, module],
924 stdout=dev_null, stderr=dev_null)
925 os.waitpid(modprobe.pid, 0)
928 def myurlgrab(url, filename, proxies):
930 if url.startswith("file:///"):
931 file = url.replace("file://", "")
932 if not os.path.exists(file):
933 raise CreatorError("URLGrabber error: can't find file %s" % file)
934 copycmd = find_binary_path("cp")
935 subprocess.call([copycmd, "-f", file, filename])
938 filename = g.urlgrab(url = url, filename = filename,
939 ssl_verify_host = False, ssl_verify_peer = False,
940 proxies = proxies, http_headers = (('Pragma', 'no-cache'),))
941 except URLGrabError, e:
942 raise CreatorError("URLGrabber error: %s: %s" % (e, url))
944 raise CreatorError("URLGrabber error: %s" % url)