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.
33 from urlgrabber.grabber import URLGrabber, 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):
47 return os.path.basename(url)[0:width]
49 class TextProgress(object):
50 def __init__(self, totalnum = None):
54 def start(self, filename, url, *args, **kwargs):
56 self.termwidth = terminal_width()
57 msger.info("\r%-*s" % (self.termwidth, " "))
58 if self.total is None:
59 msger.info("\rRetrieving %s ..." % truncate_url(self.url, self.termwidth - 15))
61 msger.info("\rRetrieving %s [%d/%d] ..." % (truncate_url(self.url, self.termwidth - 25), self.counter, self.total))
63 def update(self, *args):
67 if self.counter == self.total:
72 def find_binary_path(binary):
73 if os.environ.has_key("PATH"):
74 paths = os.environ["PATH"].split(":")
77 if os.environ.has_key("HOME"):
78 paths += [os.environ["HOME"] + "/bin"]
79 paths += ["/usr/local/sbin", "/usr/local/bin", "/usr/sbin", "/usr/bin", "/sbin", "/bin"]
82 bin_path = "%s/%s" % (path, binary)
83 if os.path.exists(bin_path):
85 raise CreatorError("Command '%s' is not available." % binary)
87 def makedirs(dirname):
88 """A version of os.makedirs() that doesn't throw an
89 exception if the leaf directory already exists.
93 except OSError, (err, msg):
94 if err != errno.EEXIST:
97 def mksquashfs(in_img, out_img):
98 fullpathmksquashfs = find_binary_path("mksquashfs")
99 args = [fullpathmksquashfs, in_img, out_img]
101 if not sys.stdout.isatty():
102 args.append("-no-progress")
104 ret = runner.show(args)
106 raise SquashfsError("'%s' exited with error (%d)" % (' '.join(args), ret))
108 def resize2fs(fs, size):
109 resize2fs = find_binary_path("resize2fs")
110 return runner.quiet([resize2fs, fs, "%sK" % (size / 1024,)])
114 fuser = find_binary_path("fuser")
115 if not os.path.exists(file):
118 dev_null = os.open("/dev/null", os.O_WRONLY)
119 rc = runner.quiet([fuser, "-s", file])
121 fuser_proc = subprocess.Popen([fuser, file], stdout=subprocess.PIPE, stderr=dev_null)
122 pids = fuser_proc.communicate()[0].strip().split()
124 fd = open("/proc/%s/cmdline" % pid, "r")
127 if cmdline[:-1] == "/bin/bash":
133 class BindChrootMount:
134 """Represents a bind mount of a directory into a chroot."""
135 def __init__(self, src, chroot, dest = None, option = None):
137 self.root = os.path.abspath(os.path.expanduser(chroot))
142 self.dest = self.root + "/" + dest
145 self.mountcmd = find_binary_path("mount")
146 self.umountcmd = find_binary_path("umount")
150 dev_null = os.open("/dev/null", os.O_WRONLY)
151 catcmd = find_binary_path("cat")
152 args = [ catcmd, "/proc/mounts" ]
153 proc_mounts = subprocess.Popen(args, stdout=subprocess.PIPE, stderr=dev_null)
154 outputs = proc_mounts.communicate()[0].strip().split("\n")
156 if line.split()[1] == os.path.abspath(self.dest):
162 def has_chroot_instance(self):
163 lock = os.path.join(self.root, ".chroot.lock")
164 return my_fuser(lock)
167 if self.mounted or self.ismounted():
171 rc = runner.show([self.mountcmd, "--bind", self.src, self.dest])
173 raise MountError("Bind-mounting '%s' to '%s' failed" %
174 (self.src, self.dest))
176 rc = runner.show([self.mountcmd, "--bind", "-o", "remount,%s" % self.option, self.dest])
178 raise MountError("Bind-remounting '%s' failed" % self.dest)
182 if self.has_chroot_instance():
186 runner.show([self.umountcmd, "-l", self.dest])
190 """LoopbackMount compatibility layer for old API"""
191 def __init__(self, lofile, mountdir, fstype = None):
192 self.diskmount = DiskMount(LoopbackDisk(lofile,size = 0),mountdir,fstype,rmmountdir = True)
194 self.losetupcmd = find_binary_path("losetup")
197 self.diskmount.cleanup()
200 self.diskmount.unmount()
204 runner.show([self.losetupcmd, "-d", self.loopdev])
212 losetupProc = subprocess.Popen([self.losetupcmd, "-f"],
213 stdout=subprocess.PIPE)
214 losetupOutput = losetupProc.communicate()[0]
216 if losetupProc.returncode:
217 raise MountError("Failed to allocate loop device for '%s'" %
220 self.loopdev = losetupOutput.split()[0]
222 rc = runner.show([self.losetupcmd, self.loopdev, self.lofile])
224 raise MountError("Failed to allocate loop device for '%s'" %
230 self.diskmount.mount()
232 class SparseLoopbackMount(LoopbackMount):
233 """SparseLoopbackMount compatibility layer for old API"""
234 def __init__(self, lofile, mountdir, size, fstype = None):
235 self.diskmount = DiskMount(SparseLoopbackDisk(lofile,size),mountdir,fstype,rmmountdir = True)
237 def expand(self, create = False, size = None):
238 self.diskmount.disk.expand(create, size)
240 def truncate(self, size = None):
241 self.diskmount.disk.truncate(size)
244 self.diskmount.disk.create()
246 class SparseExtLoopbackMount(SparseLoopbackMount):
247 """SparseExtLoopbackMount compatibility layer for old API"""
248 def __init__(self, lofile, mountdir, size, fstype, blocksize, fslabel):
249 self.diskmount = ExtDiskMount(SparseLoopbackDisk(lofile,size), mountdir, fstype, blocksize, fslabel, rmmountdir = True)
252 def __format_filesystem(self):
253 self.diskmount.__format_filesystem()
256 self.diskmount.disk.create()
258 def resize(self, size = None):
259 return self.diskmount.__resize_filesystem(size)
262 self.diskmount.mount()
265 self.extdiskmount.__fsck()
267 def __get_size_from_filesystem(self):
268 return self.diskmount.__get_size_from_filesystem()
270 def __resize_to_minimal(self):
271 return self.diskmount.__resize_to_minimal()
273 def resparse(self, size = None):
274 return self.diskmount.resparse(size)
277 """Generic base object for a disk
279 The 'create' method must make the disk visible as a block device - eg
280 by calling losetup. For RawDisk, this is obviously a no-op. The 'cleanup'
281 method must undo the 'create' operation.
283 def __init__(self, size, device = None):
284 self._device = device
293 def get_device(self):
295 def set_device(self, path):
297 device = property(get_device, set_device)
301 size = property(get_size)
305 """A Disk backed by a block device.
306 Note that create() is a no-op.
308 def __init__(self, size, device):
309 Disk.__init__(self, size, device)
317 class LoopbackDisk(Disk):
318 """A Disk backed by a file via the loop module."""
319 def __init__(self, lofile, size):
320 Disk.__init__(self, size)
322 self.losetupcmd = find_binary_path("losetup")
328 return os.path.exists(self.lofile)
331 if self.device is not None:
334 losetupProc = subprocess.Popen([self.losetupcmd, "-f"],
335 stdout=subprocess.PIPE)
336 losetupOutput = losetupProc.communicate()[0]
338 if losetupProc.returncode:
339 raise MountError("Failed to allocate loop device for '%s'" %
342 device = losetupOutput.split()[0]
344 msger.debug("Losetup add %s mapping to %s" % (device, self.lofile))
345 rc = runner.show([self.losetupcmd, device, self.lofile])
347 raise MountError("Failed to allocate loop device for '%s'" %
352 if self.device is None:
354 msger.debug("Losetup remove %s" % self.device)
355 rc = runner.show([self.losetupcmd, "-d", self.device])
358 class SparseLoopbackDisk(LoopbackDisk):
359 """A Disk backed by a sparse file via the loop module."""
360 def __init__(self, lofile, size):
361 LoopbackDisk.__init__(self, lofile, size)
363 def expand(self, create = False, size = None):
367 if not os.path.exists(self.lofile):
368 makedirs(os.path.dirname(self.lofile))
373 msger.debug("Extending sparse file %s to %d" % (self.lofile, size))
375 fd = os.open(self.lofile, flags, 0644)
377 fd = os.open(self.lofile, flags)
379 os.lseek(fd, size, os.SEEK_SET)
383 def truncate(self, size = None):
387 msger.debug("Truncating sparse file %s to %d" % (self.lofile, size))
388 fd = os.open(self.lofile, os.O_WRONLY)
389 os.ftruncate(fd, size)
393 self.expand(create = True)
394 LoopbackDisk.create(self)
397 """A generic base class to deal with mounting things."""
398 def __init__(self, mountdir):
399 self.mountdir = mountdir
404 def mount(self, options = None):
410 class DiskMount(Mount):
411 """A Mount object that handles mounting of a Disk."""
412 def __init__(self, disk, mountdir, fstype = None, rmmountdir = True):
413 Mount.__init__(self, mountdir)
417 self.rmmountdir = rmmountdir
422 self.mkfscmd = find_binary_path("mkfs." + self.fstype)
425 self.mountcmd = find_binary_path("mount")
426 self.umountcmd = find_binary_path("umount")
434 msger.debug("Unmounting directory %s" % self.mountdir)
435 runner.quiet('sync') # sync the data on this mount point
436 rc = runner.show([self.umountcmd, "-l", self.mountdir])
440 raise MountError("Failed to umount %s" % self.mountdir)
441 if self.rmdir and not self.mounted:
443 os.rmdir(self.mountdir)
453 def mount(self, options = None):
457 if not os.path.isdir(self.mountdir):
458 msger.debug("Creating mount point %s" % self.mountdir)
459 os.makedirs(self.mountdir)
460 self.rmdir = self.rmmountdir
464 msger.debug("Mounting %s at %s" % (self.disk.device, self.mountdir))
466 args = [ self.mountcmd, "-o", options, self.disk.device, self.mountdir ]
468 args = [ self.mountcmd, self.disk.device, self.mountdir ]
470 args.extend(["-t", self.fstype])
472 rc = runner.show(args)
474 raise MountError("Failed to mount '%s' to '%s' with command '%s'. Retval: %s" %
475 (self.disk.device, self.mountdir, " ".join(args), rc))
479 class ExtDiskMount(DiskMount):
480 """A DiskMount object that is able to format/resize ext[23] filesystems."""
481 def __init__(self, disk, mountdir, fstype, blocksize, fslabel, rmmountdir=True, skipformat = False, fsopts = None):
482 DiskMount.__init__(self, disk, mountdir, fstype, rmmountdir)
483 self.blocksize = blocksize
484 self.fslabel = fslabel.replace("/", "")
486 self.skipformat = skipformat
488 self.dumpe2fs = find_binary_path("dumpe2fs")
489 self.tune2fs = find_binary_path("tune2fs")
491 def __parse_field(self, output, field):
492 for line in output.split("\n"):
493 if line.startswith(field + ":"):
494 return line[len(field) + 1:].strip()
496 raise KeyError("Failed to find field '%s' in output" % field)
498 def __format_filesystem(self):
500 msger.debug("Skip filesystem format.")
503 msger.verbose("Formating %s filesystem on %s" % (self.fstype, self.disk.device))
504 rc = runner.show([self.mkfscmd,
505 "-F", "-L", self.fslabel,
506 "-m", "1", "-b", str(self.blocksize),
507 self.disk.device]) # str(self.disk.size / self.blocksize)])
509 raise MountError("Error creating %s filesystem on disk %s" % (self.fstype, self.disk.device))
511 dev_null = os.open("/dev/null", os.O_WRONLY)
513 out = subprocess.Popen([self.dumpe2fs, '-h', self.disk.device],
514 stdout = subprocess.PIPE,
515 stderr = dev_null).communicate()[0]
519 self.uuid = self.__parse_field(out, "Filesystem UUID")
520 msger.debug("Tuning filesystem on %s" % self.disk.device)
521 runner.show([self.tune2fs, "-c0", "-i0", "-Odir_index", "-ouser_xattr,acl", self.disk.device])
523 def __resize_filesystem(self, size = None):
524 current_size = os.stat(self.disk.lofile)[stat.ST_SIZE]
527 size = self.disk.size
529 if size == current_size:
532 if size > current_size:
533 self.disk.expand(size)
537 resize2fs(self.disk.lofile, size)
542 if not self.disk.fixed() and self.disk.exists():
548 self.__resize_filesystem()
550 self.__format_filesystem()
552 def mount(self, options = None):
554 DiskMount.mount(self, options)
557 msger.info("Checking filesystem %s" % self.disk.lofile)
558 runner.show(["/sbin/e2fsck", "-f", "-y", self.disk.lofile])
560 def __get_size_from_filesystem(self):
561 dev_null = os.open("/dev/null", os.O_WRONLY)
563 out = subprocess.Popen([self.dumpe2fs, '-h', self.disk.lofile],
564 stdout = subprocess.PIPE,
565 stderr = dev_null).communicate()[0]
569 return int(self.__parse_field(out, "Block count")) * self.blocksize
571 def __resize_to_minimal(self):
575 # Use a binary search to find the minimal size
576 # we can resize the image to
579 top = self.__get_size_from_filesystem()
580 while top != (bot + 1):
581 t = bot + ((top - bot) / 2)
583 if not resize2fs(self.disk.lofile, t):
589 def resparse(self, size = None):
591 minsize = self.__resize_to_minimal()
592 self.disk.truncate(minsize)
593 self.__resize_filesystem(size)
596 class VfatDiskMount(DiskMount):
597 """A DiskMount object that is able to format vfat/msdos filesystems."""
598 def __init__(self, disk, mountdir, fstype, blocksize, fslabel, rmmountdir=True, skipformat = False, fsopts = None):
599 DiskMount.__init__(self, disk, mountdir, fstype, rmmountdir)
600 self.blocksize = blocksize
601 self.fslabel = fslabel.replace("/", "")
602 self.uuid = "%08X" % int(time.time())
603 self.skipformat = skipformat
605 self.fsckcmd = find_binary_path("fsck." + self.fstype)
607 def __format_filesystem(self):
609 msger.debug("Skip filesystem format.")
612 msger.verbose("Formating %s filesystem on %s" % (self.fstype, self.disk.device))
613 rc = runner.show([self.mkfscmd, "-n", self.fslabel, "-i", self.uuid, self.disk.device])
615 raise MountError("Error creating %s filesystem on disk %s" % (self.fstype,self.disk.device))
617 msger.verbose("Tuning filesystem on %s" % self.disk.device)
619 def __resize_filesystem(self, size = None):
620 current_size = os.stat(self.disk.lofile)[stat.ST_SIZE]
623 size = self.disk.size
625 if size == current_size:
628 if size > current_size:
629 self.disk.expand(size)
633 #resize2fs(self.disk.lofile, size)
638 if not self.disk.fixed() and self.disk.exists():
644 self.__resize_filesystem()
646 self.__format_filesystem()
648 def mount(self, options = None):
650 DiskMount.mount(self, options)
653 msger.debug("Checking filesystem %s" % self.disk.lofile)
654 runner.show([self.fsckcmd, "-y", self.disk.lofile])
656 def __get_size_from_filesystem(self):
657 return self.disk.size
659 def __resize_to_minimal(self):
663 # Use a binary search to find the minimal size
664 # we can resize the image to
667 top = self.__get_size_from_filesystem()
670 def resparse(self, size = None):
672 minsize = self.__resize_to_minimal()
673 self.disk.truncate(minsize)
674 self.__resize_filesystem(size)
677 class BtrfsDiskMount(DiskMount):
678 """A DiskMount object that is able to format/resize btrfs filesystems."""
679 def __init__(self, disk, mountdir, fstype, blocksize, fslabel, rmmountdir=True, skipformat = False, fsopts = None):
681 DiskMount.__init__(self, disk, mountdir, fstype, rmmountdir)
682 self.blocksize = blocksize
683 self.fslabel = fslabel.replace("/", "")
685 self.skipformat = skipformat
687 self.blkidcmd = find_binary_path("blkid")
688 self.btrfsckcmd = find_binary_path("btrfsck")
690 def __check_btrfs(self):
692 """ Need to load btrfs module to mount it """
694 for line in open("/proc/filesystems").xreadlines():
695 if line.find("btrfs") > -1:
699 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.")
701 # disable selinux, selinux will block write
702 if os.path.exists("/usr/sbin/setenforce"):
703 runner.show(["/usr/sbin/setenforce", "0"])
705 def __parse_field(self, output, field):
706 for line in output.split(" "):
707 if line.startswith(field + "="):
708 return line[len(field) + 1:].strip().replace("\"", "")
710 raise KeyError("Failed to find field '%s' in output" % field)
712 def __format_filesystem(self):
714 msger.debug("Skip filesystem format.")
717 msger.verbose("Formating %s filesystem on %s" % (self.fstype, self.disk.device))
718 rc = runner.show([self.mkfscmd, "-L", self.fslabel, self.disk.device])
720 raise MountError("Error creating %s filesystem on disk %s" % (self.fstype,self.disk.device))
722 dev_null = os.open("/dev/null", os.O_WRONLY)
724 out = subprocess.Popen([self.blkidcmd, self.disk.device],
725 stdout = subprocess.PIPE,
726 stderr = dev_null).communicate()[0]
730 self.uuid = self.__parse_field(out, "UUID")
732 def __resize_filesystem(self, size = None):
733 current_size = os.stat(self.disk.lofile)[stat.ST_SIZE]
736 size = self.disk.size
738 if size == current_size:
741 if size > current_size:
742 self.disk.expand(size)
749 if not self.disk.fixed() and self.disk.exists():
755 self.__resize_filesystem()
757 self.__format_filesystem()
759 def mount(self, options = None):
761 DiskMount.mount(self, options)
764 msger.debug("Checking filesystem %s" % self.disk.lofile)
765 runner.show([self.btrfsckcmd, self.disk.lofile])
767 def __get_size_from_filesystem(self):
768 return self.disk.size
770 def __resize_to_minimal(self):
773 return self.__get_size_from_filesystem()
775 def resparse(self, size = None):
777 minsize = self.__resize_to_minimal()
778 self.disk.truncate(minsize)
779 self.__resize_filesystem(size)
782 class DeviceMapperSnapshot(object):
783 def __init__(self, imgloop, cowloop):
784 self.imgloop = imgloop
785 self.cowloop = cowloop
787 self.__created = False
789 self.dmsetupcmd = find_binary_path("dmsetup")
791 """Load dm_snapshot if it isn't loaded"""
792 load_module("dm_snapshot")
795 if self.__name is None:
797 return os.path.join("/dev/mapper", self.__name)
798 path = property(get_path)
804 self.imgloop.create()
805 self.cowloop.create()
807 self.__name = "imgcreate-%d-%d" % (os.getpid(),
808 random.randint(0, 2**16))
810 size = os.stat(self.imgloop.lofile)[stat.ST_SIZE]
812 table = "0 %d snapshot %s %s p 8" % (size / 512,
816 args = [self.dmsetupcmd, "create", self.__name, "--table", table]
817 if runner.show(args) != 0:
818 self.cowloop.cleanup()
819 self.imgloop.cleanup()
820 raise SnapshotError("Could not create snapshot device using: " + ' '.join(args))
822 self.__created = True
824 def remove(self, ignore_errors = False):
825 if not self.__created:
829 rc = runner.show([self.dmsetupcmd, "remove", self.__name])
830 if not ignore_errors and rc != 0:
831 raise SnapshotError("Could not remove snapshot device")
834 self.__created = False
836 self.cowloop.cleanup()
837 self.imgloop.cleanup()
839 def get_cow_used(self):
840 if not self.__created:
843 dev_null = os.open("/dev/null", os.O_WRONLY)
845 out = subprocess.Popen([self.dmsetupcmd, "status", self.__name],
846 stdout = subprocess.PIPE,
847 stderr = dev_null).communicate()[0]
852 # dmsetup status on a snapshot returns e.g.
853 # "0 8388608 snapshot 416/1048576"
854 # or, more generally:
856 # where C is the number of 512 byte sectors in use
859 return int((out.split()[3]).split('/')[0]) * 512
861 raise SnapshotError("Failed to parse dmsetup status: " + out)
863 def create_image_minimizer(path, image, minimal_size):
865 Builds a copy-on-write image which can be used to
866 create a device-mapper snapshot of an image where
867 the image's filesystem is as small as possible
870 1) Create a sparse COW
871 2) Loopback mount the image and the COW
872 3) Create a device-mapper snapshot of the image
874 4) Resize the filesystem to the minimal size
875 5) Determine the amount of space used in the COW
876 6) Restroy the device-mapper snapshot
877 7) Truncate the COW, removing unused space
878 8) Create a squashfs of the COW
880 imgloop = LoopbackDisk(image, None) # Passing bogus size - doesn't matter
882 cowloop = SparseLoopbackDisk(os.path.join(os.path.dirname(path), "osmin"),
885 snapshot = DeviceMapperSnapshot(imgloop, cowloop)
890 resize2fs(snapshot.path, minimal_size)
892 cow_used = snapshot.get_cow_used()
894 snapshot.remove(ignore_errors = (not sys.exc_info()[0] is None))
896 cowloop.truncate(cow_used)
898 mksquashfs(cowloop.lofile, path)
900 os.unlink(cowloop.lofile)
902 def load_module(module):
904 for line in open('/proc/modules').xreadlines():
905 if line.startswith("%s " % module):
909 msger.info("Loading %s..." % module)
910 runner.quiet(['modprobe', module])
912 def myurlgrab(url, filename, proxies, progress_obj = None):
914 if progress_obj is None:
915 progress_obj = TextProgress()
917 if url.startswith("file:///"):
918 file = url.replace("file://", "")
919 if not os.path.exists(file):
920 raise CreatorError("URLGrabber error: can't find file %s" % file)
921 runner.show(['cp', "-f", file, filename])
924 filename = g.urlgrab(url = url, filename = filename,
925 ssl_verify_host = False, ssl_verify_peer = False,
926 proxies = proxies, http_headers = (('Pragma', 'no-cache'),), progress_obj = progress_obj)
927 except URLGrabError, e:
928 raise CreatorError("URLGrabber error: %s" % url)