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
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 = msger.run(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 msger.run([resize2fs, fs, "%sK" % (size / 1024,)], True)
113 fuser = find_binary_path("fuser")
114 if not os.path.exists(file):
117 dev_null = os.open("/dev/null", os.O_WRONLY)
118 rc = msger.run([fuser, "-s", file], True)
120 fuser_proc = subprocess.Popen([fuser, file], stdout=subprocess.PIPE, stderr=dev_null)
121 pids = fuser_proc.communicate()[0].strip().split()
123 fd = open("/proc/%s/cmdline" % pid, "r")
126 if cmdline[:-1] == "/bin/bash":
132 class BindChrootMount:
133 """Represents a bind mount of a directory into a chroot."""
134 def __init__(self, src, chroot, dest = None, option = None):
136 self.root = os.path.abspath(os.path.expanduser(chroot))
141 self.dest = self.root + "/" + dest
144 self.mountcmd = find_binary_path("mount")
145 self.umountcmd = find_binary_path("umount")
149 dev_null = os.open("/dev/null", os.O_WRONLY)
150 catcmd = find_binary_path("cat")
151 args = [ catcmd, "/proc/mounts" ]
152 proc_mounts = subprocess.Popen(args, stdout=subprocess.PIPE, stderr=dev_null)
153 outputs = proc_mounts.communicate()[0].strip().split("\n")
155 if line.split()[1] == os.path.abspath(self.dest):
161 def has_chroot_instance(self):
162 lock = os.path.join(self.root, ".chroot.lock")
163 return my_fuser(lock)
166 if self.mounted or self.ismounted():
170 rc = msger.run([self.mountcmd, "--bind", self.src, self.dest])
172 raise MountError("Bind-mounting '%s' to '%s' failed" %
173 (self.src, self.dest))
175 rc = msger.run([self.mountcmd, "--bind", "-o", "remount,%s" % self.option, self.dest])
177 raise MountError("Bind-remounting '%s' failed" % self.dest)
181 if self.has_chroot_instance():
185 msger.run([self.umountcmd, "-l", self.dest])
189 """LoopbackMount compatibility layer for old API"""
190 def __init__(self, lofile, mountdir, fstype = None):
191 self.diskmount = DiskMount(LoopbackDisk(lofile,size = 0),mountdir,fstype,rmmountdir = True)
193 self.losetupcmd = find_binary_path("losetup")
196 self.diskmount.cleanup()
199 self.diskmount.unmount()
203 msger.run([self.losetupcmd, "-d", self.loopdev])
211 losetupProc = subprocess.Popen([self.losetupcmd, "-f"],
212 stdout=subprocess.PIPE)
213 losetupOutput = losetupProc.communicate()[0]
215 if losetupProc.returncode:
216 raise MountError("Failed to allocate loop device for '%s'" %
219 self.loopdev = losetupOutput.split()[0]
221 rc = msger.run([self.losetupcmd, self.loopdev, self.lofile])
223 raise MountError("Failed to allocate loop device for '%s'" %
229 self.diskmount.mount()
231 class SparseLoopbackMount(LoopbackMount):
232 """SparseLoopbackMount compatibility layer for old API"""
233 def __init__(self, lofile, mountdir, size, fstype = None):
234 self.diskmount = DiskMount(SparseLoopbackDisk(lofile,size),mountdir,fstype,rmmountdir = True)
236 def expand(self, create = False, size = None):
237 self.diskmount.disk.expand(create, size)
239 def truncate(self, size = None):
240 self.diskmount.disk.truncate(size)
243 self.diskmount.disk.create()
245 class SparseExtLoopbackMount(SparseLoopbackMount):
246 """SparseExtLoopbackMount compatibility layer for old API"""
247 def __init__(self, lofile, mountdir, size, fstype, blocksize, fslabel):
248 self.diskmount = ExtDiskMount(SparseLoopbackDisk(lofile,size), mountdir, fstype, blocksize, fslabel, rmmountdir = True)
251 def __format_filesystem(self):
252 self.diskmount.__format_filesystem()
255 self.diskmount.disk.create()
257 def resize(self, size = None):
258 return self.diskmount.__resize_filesystem(size)
261 self.diskmount.mount()
264 self.extdiskmount.__fsck()
266 def __get_size_from_filesystem(self):
267 return self.diskmount.__get_size_from_filesystem()
269 def __resize_to_minimal(self):
270 return self.diskmount.__resize_to_minimal()
272 def resparse(self, size = None):
273 return self.diskmount.resparse(size)
276 """Generic base object for a disk
278 The 'create' method must make the disk visible as a block device - eg
279 by calling losetup. For RawDisk, this is obviously a no-op. The 'cleanup'
280 method must undo the 'create' operation.
282 def __init__(self, size, device = None):
283 self._device = device
292 def get_device(self):
294 def set_device(self, path):
296 device = property(get_device, set_device)
300 size = property(get_size)
304 """A Disk backed by a block device.
305 Note that create() is a no-op.
307 def __init__(self, size, device):
308 Disk.__init__(self, size, device)
316 class LoopbackDisk(Disk):
317 """A Disk backed by a file via the loop module."""
318 def __init__(self, lofile, size):
319 Disk.__init__(self, size)
321 self.losetupcmd = find_binary_path("losetup")
327 return os.path.exists(self.lofile)
330 if self.device is not None:
333 losetupProc = subprocess.Popen([self.losetupcmd, "-f"],
334 stdout=subprocess.PIPE)
335 losetupOutput = losetupProc.communicate()[0]
337 if losetupProc.returncode:
338 raise MountError("Failed to allocate loop device for '%s'" %
341 device = losetupOutput.split()[0]
343 msger.debug("Losetup add %s mapping to %s" % (device, self.lofile))
344 rc = msger.run([self.losetupcmd, device, self.lofile])
346 raise MountError("Failed to allocate loop device for '%s'" %
351 if self.device is None:
353 msger.debug("Losetup remove %s" % self.device)
354 rc = msger.run([self.losetupcmd, "-d", self.device])
357 class SparseLoopbackDisk(LoopbackDisk):
358 """A Disk backed by a sparse file via the loop module."""
359 def __init__(self, lofile, size):
360 LoopbackDisk.__init__(self, lofile, size)
362 def expand(self, create = False, size = None):
366 if not os.path.exists(self.lofile):
367 makedirs(os.path.dirname(self.lofile))
372 msger.debug("Extending sparse file %s to %d" % (self.lofile, size))
374 fd = os.open(self.lofile, flags, 0644)
376 fd = os.open(self.lofile, flags)
378 os.lseek(fd, size, os.SEEK_SET)
382 def truncate(self, size = None):
386 msger.debug("Truncating sparse file %s to %d" % (self.lofile, size))
387 fd = os.open(self.lofile, os.O_WRONLY)
388 os.ftruncate(fd, size)
392 self.expand(create = True)
393 LoopbackDisk.create(self)
396 """A generic base class to deal with mounting things."""
397 def __init__(self, mountdir):
398 self.mountdir = mountdir
403 def mount(self, options = None):
409 class DiskMount(Mount):
410 """A Mount object that handles mounting of a Disk."""
411 def __init__(self, disk, mountdir, fstype = None, rmmountdir = True):
412 Mount.__init__(self, mountdir)
416 self.rmmountdir = rmmountdir
421 self.mkfscmd = find_binary_path("mkfs." + self.fstype)
424 self.mountcmd = find_binary_path("mount")
425 self.umountcmd = find_binary_path("umount")
433 msger.debug("Unmounting directory %s" % self.mountdir)
434 msger.run('sync', True) # sync the data on this mount point
435 rc = msger.run([self.umountcmd, "-l", self.mountdir])
439 raise MountError("Failed to umount %s" % self.mountdir)
440 if self.rmdir and not self.mounted:
442 os.rmdir(self.mountdir)
452 def mount(self, options = None):
456 if not os.path.isdir(self.mountdir):
457 msger.debug("Creating mount point %s" % self.mountdir)
458 os.makedirs(self.mountdir)
459 self.rmdir = self.rmmountdir
463 msger.debug("Mounting %s at %s" % (self.disk.device, self.mountdir))
465 args = [ self.mountcmd, "-o", options, self.disk.device, self.mountdir ]
467 args = [ self.mountcmd, self.disk.device, self.mountdir ]
469 args.extend(["-t", self.fstype])
473 raise MountError("Failed to mount '%s' to '%s' with command '%s'. Retval: %s" %
474 (self.disk.device, self.mountdir, " ".join(args), rc))
478 class ExtDiskMount(DiskMount):
479 """A DiskMount object that is able to format/resize ext[23] filesystems."""
480 def __init__(self, disk, mountdir, fstype, blocksize, fslabel, rmmountdir=True, skipformat = False, fsopts = None):
481 DiskMount.__init__(self, disk, mountdir, fstype, rmmountdir)
482 self.blocksize = blocksize
483 self.fslabel = fslabel.replace("/", "")
485 self.skipformat = skipformat
487 self.dumpe2fs = find_binary_path("dumpe2fs")
488 self.tune2fs = find_binary_path("tune2fs")
490 def __parse_field(self, output, field):
491 for line in output.split("\n"):
492 if line.startswith(field + ":"):
493 return line[len(field) + 1:].strip()
495 raise KeyError("Failed to find field '%s' in output" % field)
497 def __format_filesystem(self):
499 msger.debug("Skip filesystem format.")
502 msger.info("Formating %s filesystem on %s" % (self.fstype, self.disk.device))
503 rc = msger.run([self.mkfscmd,
504 "-F", "-L", self.fslabel,
505 "-m", "1", "-b", str(self.blocksize),
506 self.disk.device]) # str(self.disk.size / self.blocksize)])
508 raise MountError("Error creating %s filesystem on disk %s" % (self.fstype, self.disk.device))
510 dev_null = os.open("/dev/null", os.O_WRONLY)
512 out = subprocess.Popen([self.dumpe2fs, '-h', self.disk.device],
513 stdout = subprocess.PIPE,
514 stderr = dev_null).communicate()[0]
518 self.uuid = self.__parse_field(out, "Filesystem UUID")
519 msger.debug("Tuning filesystem on %s" % self.disk.device)
520 msger.run([self.tune2fs, "-c0", "-i0", "-Odir_index", "-ouser_xattr,acl", self.disk.device])
522 def __resize_filesystem(self, size = None):
523 current_size = os.stat(self.disk.lofile)[stat.ST_SIZE]
526 size = self.disk.size
528 if size == current_size:
531 if size > current_size:
532 self.disk.expand(size)
536 resize2fs(self.disk.lofile, size)
541 if not self.disk.fixed() and self.disk.exists():
547 self.__resize_filesystem()
549 self.__format_filesystem()
551 def mount(self, options = None):
553 DiskMount.mount(self, options)
556 msger.info("Checking filesystem %s" % self.disk.lofile)
557 msger.run(["/sbin/e2fsck", "-f", "-y", self.disk.lofile])
559 def __get_size_from_filesystem(self):
560 dev_null = os.open("/dev/null", os.O_WRONLY)
562 out = subprocess.Popen([self.dumpe2fs, '-h', self.disk.lofile],
563 stdout = subprocess.PIPE,
564 stderr = dev_null).communicate()[0]
568 return int(self.__parse_field(out, "Block count")) * self.blocksize
570 def __resize_to_minimal(self):
574 # Use a binary search to find the minimal size
575 # we can resize the image to
578 top = self.__get_size_from_filesystem()
579 while top != (bot + 1):
580 t = bot + ((top - bot) / 2)
582 if not resize2fs(self.disk.lofile, t):
588 def resparse(self, size = None):
590 minsize = self.__resize_to_minimal()
591 self.disk.truncate(minsize)
592 self.__resize_filesystem(size)
595 class VfatDiskMount(DiskMount):
596 """A DiskMount object that is able to format vfat/msdos filesystems."""
597 def __init__(self, disk, mountdir, fstype, blocksize, fslabel, rmmountdir=True, skipformat = False, fsopts = None):
598 DiskMount.__init__(self, disk, mountdir, fstype, rmmountdir)
599 self.blocksize = blocksize
600 self.fslabel = fslabel.replace("/", "")
601 self.uuid = "%08X" % int(time.time())
602 self.skipformat = skipformat
604 self.fsckcmd = find_binary_path("fsck." + self.fstype)
606 def __format_filesystem(self):
608 msger.debug("Skip filesystem format.")
610 msger.debug("Formating %s filesystem on %s" % (self.fstype, self.disk.device))
611 rc = msger.run([self.mkfscmd, "-n", self.fslabel, "-i", self.uuid, self.disk.device])
613 raise MountError("Error creating %s filesystem on disk %s" % (self.fstype,self.disk.device))
614 msger.debug("Tuning filesystem on %s" % self.disk.device)
616 def __resize_filesystem(self, size = None):
617 current_size = os.stat(self.disk.lofile)[stat.ST_SIZE]
620 size = self.disk.size
622 if size == current_size:
625 if size > current_size:
626 self.disk.expand(size)
630 #resize2fs(self.disk.lofile, size)
635 if not self.disk.fixed() and self.disk.exists():
641 self.__resize_filesystem()
643 self.__format_filesystem()
645 def mount(self, options = None):
647 DiskMount.mount(self, options)
650 msger.debug("Checking filesystem %s" % self.disk.lofile)
651 msger.run([self.fsckcmd, "-y", self.disk.lofile])
653 def __get_size_from_filesystem(self):
654 return self.disk.size
656 def __resize_to_minimal(self):
660 # Use a binary search to find the minimal size
661 # we can resize the image to
664 top = self.__get_size_from_filesystem()
667 def resparse(self, size = None):
669 minsize = self.__resize_to_minimal()
670 self.disk.truncate(minsize)
671 self.__resize_filesystem(size)
674 class BtrfsDiskMount(DiskMount):
675 """A DiskMount object that is able to format/resize btrfs filesystems."""
676 def __init__(self, disk, mountdir, fstype, blocksize, fslabel, rmmountdir=True, skipformat = False, fsopts = None):
678 DiskMount.__init__(self, disk, mountdir, fstype, rmmountdir)
679 self.blocksize = blocksize
680 self.fslabel = fslabel.replace("/", "")
682 self.skipformat = skipformat
684 self.blkidcmd = find_binary_path("blkid")
685 self.btrfsckcmd = find_binary_path("btrfsck")
687 def __check_btrfs(self):
689 """ Need to load btrfs module to mount it """
691 for line in open("/proc/filesystems").xreadlines():
692 if line.find("btrfs") > -1:
696 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.")
698 # disable selinux, selinux will block write
699 if os.path.exists("/usr/sbin/setenforce"):
700 msger.run(["/usr/sbin/setenforce", "0"])
702 def __parse_field(self, output, field):
703 for line in output.split(" "):
704 if line.startswith(field + "="):
705 return line[len(field) + 1:].strip().replace("\"", "")
707 raise KeyError("Failed to find field '%s' in output" % field)
709 def __format_filesystem(self):
711 msger.debug("Skip filesystem format.")
713 msger.debug("Formating %s filesystem on %s" % (self.fstype, self.disk.device))
714 rc = msger.run([self.mkfscmd, "-L", self.fslabel, self.disk.device])
716 raise MountError("Error creating %s filesystem on disk %s" % (self.fstype,self.disk.device))
718 dev_null = os.open("/dev/null", os.O_WRONLY)
720 out = subprocess.Popen([self.blkidcmd, self.disk.device],
721 stdout = subprocess.PIPE,
722 stderr = dev_null).communicate()[0]
726 self.uuid = self.__parse_field(out, "UUID")
728 def __resize_filesystem(self, size = None):
729 current_size = os.stat(self.disk.lofile)[stat.ST_SIZE]
732 size = self.disk.size
734 if size == current_size:
737 if size > current_size:
738 self.disk.expand(size)
745 if not self.disk.fixed() and self.disk.exists():
751 self.__resize_filesystem()
753 self.__format_filesystem()
755 def mount(self, options = None):
757 DiskMount.mount(self, options)
760 msger.debug("Checking filesystem %s" % self.disk.lofile)
761 msger.run([self.btrfsckcmd, self.disk.lofile])
763 def __get_size_from_filesystem(self):
764 return self.disk.size
766 def __resize_to_minimal(self):
769 return self.__get_size_from_filesystem()
771 def resparse(self, size = None):
773 minsize = self.__resize_to_minimal()
774 self.disk.truncate(minsize)
775 self.__resize_filesystem(size)
778 class DeviceMapperSnapshot(object):
779 def __init__(self, imgloop, cowloop):
780 self.imgloop = imgloop
781 self.cowloop = cowloop
783 self.__created = False
785 self.dmsetupcmd = find_binary_path("dmsetup")
787 """Load dm_snapshot if it isn't loaded"""
788 load_module("dm_snapshot")
791 if self.__name is None:
793 return os.path.join("/dev/mapper", self.__name)
794 path = property(get_path)
800 self.imgloop.create()
801 self.cowloop.create()
803 self.__name = "imgcreate-%d-%d" % (os.getpid(),
804 random.randint(0, 2**16))
806 size = os.stat(self.imgloop.lofile)[stat.ST_SIZE]
808 table = "0 %d snapshot %s %s p 8" % (size / 512,
812 args = [self.dmsetupcmd, "create", self.__name, "--table", table]
813 if msger.run(args) != 0:
814 self.cowloop.cleanup()
815 self.imgloop.cleanup()
816 raise SnapshotError("Could not create snapshot device using: " + ' '.join(args))
818 self.__created = True
820 def remove(self, ignore_errors = False):
821 if not self.__created:
825 rc = msger.run([self.dmsetupcmd, "remove", self.__name])
826 if not ignore_errors and rc != 0:
827 raise SnapshotError("Could not remove snapshot device")
830 self.__created = False
832 self.cowloop.cleanup()
833 self.imgloop.cleanup()
835 def get_cow_used(self):
836 if not self.__created:
839 dev_null = os.open("/dev/null", os.O_WRONLY)
841 out = subprocess.Popen([self.dmsetupcmd, "status", self.__name],
842 stdout = subprocess.PIPE,
843 stderr = dev_null).communicate()[0]
848 # dmsetup status on a snapshot returns e.g.
849 # "0 8388608 snapshot 416/1048576"
850 # or, more generally:
852 # where C is the number of 512 byte sectors in use
855 return int((out.split()[3]).split('/')[0]) * 512
857 raise SnapshotError("Failed to parse dmsetup status: " + out)
859 def create_image_minimizer(path, image, minimal_size):
861 Builds a copy-on-write image which can be used to
862 create a device-mapper snapshot of an image where
863 the image's filesystem is as small as possible
866 1) Create a sparse COW
867 2) Loopback mount the image and the COW
868 3) Create a device-mapper snapshot of the image
870 4) Resize the filesystem to the minimal size
871 5) Determine the amount of space used in the COW
872 6) Restroy the device-mapper snapshot
873 7) Truncate the COW, removing unused space
874 8) Create a squashfs of the COW
876 imgloop = LoopbackDisk(image, None) # Passing bogus size - doesn't matter
878 cowloop = SparseLoopbackDisk(os.path.join(os.path.dirname(path), "osmin"),
881 snapshot = DeviceMapperSnapshot(imgloop, cowloop)
886 resize2fs(snapshot.path, minimal_size)
888 cow_used = snapshot.get_cow_used()
890 snapshot.remove(ignore_errors = (not sys.exc_info()[0] is None))
892 cowloop.truncate(cow_used)
894 mksquashfs(cowloop.lofile, path)
896 os.unlink(cowloop.lofile)
898 def load_module(module):
900 for line in open('/proc/modules').xreadlines():
901 if line.startswith("%s " % module):
905 msger.info("Loading %s..." % module)
906 msger.run(['modprobe', module], True)
908 def myurlgrab(url, filename, proxies, progress_obj = None):
910 if progress_obj is None:
911 progress_obj = TextProgress()
913 if url.startswith("file:///"):
914 file = url.replace("file://", "")
915 if not os.path.exists(file):
916 raise CreatorError("URLGrabber error: can't find file %s" % file)
917 msger.run(['cp', "-f", file, filename])
920 filename = g.urlgrab(url = url, filename = filename,
921 ssl_verify_host = False, ssl_verify_peer = False,
922 proxies = proxies, http_headers = (('Pragma', 'no-cache'),), progress_obj = progress_obj)
923 except URLGrabError, e:
924 raise CreatorError("URLGrabber error: %s" % url)