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.
34 def terminal_width(fd=1):
35 """ Get the real terminal width """
38 buf = fcntl.ioctl(fd, termios.TIOCGWINSZ, buf)
39 return struct.unpack('hhhh', buf)[1]
43 def truncate_url(url, width):
44 return os.path.basename(url)[0:width]
46 class TextProgress(object):
47 # make the class as singleton
49 def __new__(cls, *args, **kwargs):
51 cls._instance = super(TextProgress, cls).__new__(cls, *args, **kwargs)
55 def __init__(self, totalnum = None):
59 def start(self, filename, url, *args, **kwargs):
61 self.termwidth = terminal_width()
62 msger.info("\r%-*s" % (self.termwidth, " "))
63 if self.total is None:
64 msger.info("\rRetrieving %s ..." % truncate_url(self.url, self.termwidth - 15))
66 msger.info("\rRetrieving %s [%d/%d] ..." % (truncate_url(self.url, self.termwidth - 25), self.counter, self.total))
68 def update(self, *args):
72 if self.counter == self.total:
75 if self.total is not None:
78 def find_binary_path(binary):
79 if os.environ.has_key("PATH"):
80 paths = os.environ["PATH"].split(":")
83 if os.environ.has_key("HOME"):
84 paths += [os.environ["HOME"] + "/bin"]
85 paths += ["/usr/local/sbin", "/usr/local/bin", "/usr/sbin", "/usr/bin", "/sbin", "/bin"]
88 bin_path = "%s/%s" % (path, binary)
89 if os.path.exists(bin_path):
91 raise CreatorError("Command '%s' is not available." % binary)
93 def makedirs(dirname):
94 """A version of os.makedirs() that doesn't throw an
95 exception if the leaf directory already exists.
99 except OSError, (err, msg):
100 if err != errno.EEXIST:
103 def mksquashfs(in_img, out_img):
104 fullpathmksquashfs = find_binary_path("mksquashfs")
105 args = [fullpathmksquashfs, in_img, out_img]
107 if not sys.stdout.isatty():
108 args.append("-no-progress")
110 ret = runner.show(args)
112 raise SquashfsError("'%s' exited with error (%d)" % (' '.join(args), ret))
114 def resize2fs(fs, size):
115 resize2fs = find_binary_path("resize2fs")
117 # it means to minimalize it
118 return runner.show([resize2fs, '-M', fs])
120 return runner.show([resize2fs, fs, "%sK" % (size / 1024,)])
123 fuser = find_binary_path("fuser")
124 if not os.path.exists(fp):
127 rc = runner.quiet([fuser, "-s", fp])
129 for pid in runner.outs([fuser, fp]).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")
155 with open('/proc/mounts') as f:
157 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 self.loopdev = get_loop_device(self.losetupcmd, self.lofile)
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 self.device = get_loop_device(self.losetupcmd, self.lofile)
323 if self.device is None:
325 msger.debug("Losetup remove %s" % self.device)
326 rc = runner.show([self.losetupcmd, "-d", self.device])
329 class SparseLoopbackDisk(LoopbackDisk):
330 """A Disk backed by a sparse file via the loop module."""
331 def __init__(self, lofile, size):
332 LoopbackDisk.__init__(self, lofile, size)
334 def expand(self, create = False, size = None):
338 if not os.path.exists(self.lofile):
339 makedirs(os.path.dirname(self.lofile))
344 msger.debug("Extending sparse file %s to %d" % (self.lofile, size))
346 fd = os.open(self.lofile, flags, 0644)
348 fd = os.open(self.lofile, flags)
350 os.lseek(fd, size, os.SEEK_SET)
354 def truncate(self, size = None):
358 msger.debug("Truncating sparse file %s to %d" % (self.lofile, size))
359 fd = os.open(self.lofile, os.O_WRONLY)
360 os.ftruncate(fd, size)
364 self.expand(create = True)
365 LoopbackDisk.create(self)
368 """A generic base class to deal with mounting things."""
369 def __init__(self, mountdir):
370 self.mountdir = mountdir
375 def mount(self, options = None):
381 class DiskMount(Mount):
382 """A Mount object that handles mounting of a Disk."""
383 def __init__(self, disk, mountdir, fstype = None, rmmountdir = True):
384 Mount.__init__(self, mountdir)
388 self.rmmountdir = rmmountdir
393 self.mkfscmd = find_binary_path("mkfs." + self.fstype)
396 self.mountcmd = find_binary_path("mount")
397 self.umountcmd = find_binary_path("umount")
405 msger.debug("Unmounting directory %s" % self.mountdir)
406 runner.quiet('sync') # sync the data on this mount point
407 rc = runner.show([self.umountcmd, "-l", self.mountdir])
411 raise MountError("Failed to umount %s" % self.mountdir)
412 if self.rmdir and not self.mounted:
414 os.rmdir(self.mountdir)
424 def mount(self, options = None):
428 if not os.path.isdir(self.mountdir):
429 msger.debug("Creating mount point %s" % self.mountdir)
430 os.makedirs(self.mountdir)
431 self.rmdir = self.rmmountdir
435 msger.debug("Mounting %s at %s" % (self.disk.device, self.mountdir))
437 args = [ self.mountcmd, "-o", options, self.disk.device, self.mountdir ]
439 args = [ self.mountcmd, self.disk.device, self.mountdir ]
441 args.extend(["-t", self.fstype])
443 rc = runner.show(args)
445 raise MountError("Failed to mount '%s' to '%s' with command '%s'. Retval: %s" %
446 (self.disk.device, self.mountdir, " ".join(args), rc))
450 class ExtDiskMount(DiskMount):
451 """A DiskMount object that is able to format/resize ext[23] filesystems."""
452 def __init__(self, disk, mountdir, fstype, blocksize, fslabel, rmmountdir=True, skipformat = False, fsopts = None):
453 DiskMount.__init__(self, disk, mountdir, fstype, rmmountdir)
454 self.blocksize = blocksize
455 self.fslabel = fslabel.replace("/", "")
457 self.skipformat = skipformat
459 self.dumpe2fs = find_binary_path("dumpe2fs")
460 self.tune2fs = find_binary_path("tune2fs")
462 def __parse_field(self, output, field):
463 for line in output.split("\n"):
464 if line.startswith(field + ":"):
465 return line[len(field) + 1:].strip()
467 raise KeyError("Failed to find field '%s' in output" % field)
469 def __format_filesystem(self):
471 msger.debug("Skip filesystem format.")
474 msger.verbose("Formating %s filesystem on %s" % (self.fstype, self.disk.device))
475 rc = runner.show([self.mkfscmd,
476 "-F", "-L", self.fslabel,
477 "-m", "1", "-b", str(self.blocksize),
478 self.disk.device]) # str(self.disk.size / self.blocksize)])
480 raise MountError("Error creating %s filesystem on disk %s" % (self.fstype, self.disk.device))
482 out = runner.outs([self.dumpe2fs, '-h', self.disk.device])
484 self.uuid = self.__parse_field(out, "Filesystem UUID")
485 msger.debug("Tuning filesystem on %s" % self.disk.device)
486 runner.show([self.tune2fs, "-c0", "-i0", "-Odir_index", "-ouser_xattr,acl", self.disk.device])
488 def __resize_filesystem(self, size = None):
489 current_size = os.stat(self.disk.lofile)[stat.ST_SIZE]
492 size = self.disk.size
494 if size == current_size:
497 if size > current_size:
498 self.disk.expand(size)
502 resize2fs(self.disk.lofile, size)
507 if not self.disk.fixed() and self.disk.exists():
513 self.__resize_filesystem()
515 self.__format_filesystem()
517 def mount(self, options = None):
519 DiskMount.mount(self, options)
522 msger.info("Checking filesystem %s" % self.disk.lofile)
523 runner.quiet(["/sbin/e2fsck", "-f", "-y", self.disk.lofile])
525 def __get_size_from_filesystem(self):
526 return int(self.__parse_field(runner.outs([self.dumpe2fs, '-h', self.disk.lofile]),
527 "Block count")) * self.blocksize
529 def __resize_to_minimal(self):
533 # Use a binary search to find the minimal size
534 # we can resize the image to
537 top = self.__get_size_from_filesystem()
538 while top != (bot + 1):
539 t = bot + ((top - bot) / 2)
541 if not resize2fs(self.disk.lofile, t):
547 def resparse(self, size = None):
552 minsize = self.__resize_to_minimal()
553 self.disk.truncate(minsize)
555 self.__resize_filesystem(size)
558 class VfatDiskMount(DiskMount):
559 """A DiskMount object that is able to format vfat/msdos filesystems."""
560 def __init__(self, disk, mountdir, fstype, blocksize, fslabel, rmmountdir=True, skipformat = False, fsopts = None):
561 DiskMount.__init__(self, disk, mountdir, fstype, rmmountdir)
562 self.blocksize = blocksize
563 self.fslabel = fslabel.replace("/", "")
564 self.uuid = "%08X" % int(time.time())
565 self.skipformat = skipformat
567 self.fsckcmd = find_binary_path("fsck." + self.fstype)
569 def __format_filesystem(self):
571 msger.debug("Skip filesystem format.")
574 msger.verbose("Formating %s filesystem on %s" % (self.fstype, self.disk.device))
575 rc = runner.show([self.mkfscmd, "-n", self.fslabel, "-i", self.uuid, self.disk.device])
577 raise MountError("Error creating %s filesystem on disk %s" % (self.fstype,self.disk.device))
579 msger.verbose("Tuning filesystem on %s" % self.disk.device)
581 def __resize_filesystem(self, size = None):
582 current_size = os.stat(self.disk.lofile)[stat.ST_SIZE]
585 size = self.disk.size
587 if size == current_size:
590 if size > current_size:
591 self.disk.expand(size)
595 #resize2fs(self.disk.lofile, size)
600 if not self.disk.fixed() and self.disk.exists():
606 self.__resize_filesystem()
608 self.__format_filesystem()
610 def mount(self, options = None):
612 DiskMount.mount(self, options)
615 msger.debug("Checking filesystem %s" % self.disk.lofile)
616 runner.show([self.fsckcmd, "-y", self.disk.lofile])
618 def __get_size_from_filesystem(self):
619 return self.disk.size
621 def __resize_to_minimal(self):
625 # Use a binary search to find the minimal size
626 # we can resize the image to
629 top = self.__get_size_from_filesystem()
632 def resparse(self, size = None):
634 minsize = self.__resize_to_minimal()
635 self.disk.truncate(minsize)
636 self.__resize_filesystem(size)
639 class BtrfsDiskMount(DiskMount):
640 """A DiskMount object that is able to format/resize btrfs filesystems."""
641 def __init__(self, disk, mountdir, fstype, blocksize, fslabel, rmmountdir=True, skipformat = False, fsopts = None):
643 DiskMount.__init__(self, disk, mountdir, fstype, rmmountdir)
644 self.blocksize = blocksize
645 self.fslabel = fslabel.replace("/", "")
647 self.skipformat = skipformat
649 self.blkidcmd = find_binary_path("blkid")
650 self.btrfsckcmd = find_binary_path("btrfsck")
652 def __check_btrfs(self):
654 """ Need to load btrfs module to mount it """
656 for line in open("/proc/filesystems").xreadlines():
657 if line.find("btrfs") > -1:
661 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.")
663 # disable selinux, selinux will block write
664 if os.path.exists("/usr/sbin/setenforce"):
665 runner.show(["/usr/sbin/setenforce", "0"])
667 def __parse_field(self, output, field):
668 for line in output.split(" "):
669 if line.startswith(field + "="):
670 return line[len(field) + 1:].strip().replace("\"", "")
672 raise KeyError("Failed to find field '%s' in output" % field)
674 def __format_filesystem(self):
676 msger.debug("Skip filesystem format.")
679 msger.verbose("Formating %s filesystem on %s" % (self.fstype, self.disk.device))
680 rc = runner.show([self.mkfscmd, "-L", self.fslabel, self.disk.device])
682 raise MountError("Error creating %s filesystem on disk %s" % (self.fstype,self.disk.device))
684 self.uuid = self.__parse_field(runner.outs([self.blkidcmd, self.disk.device]), "UUID")
686 def __resize_filesystem(self, size = None):
687 current_size = os.stat(self.disk.lofile)[stat.ST_SIZE]
690 size = self.disk.size
692 if size == current_size:
695 if size > current_size:
696 self.disk.expand(size)
703 if not self.disk.fixed() and self.disk.exists():
709 self.__resize_filesystem()
711 self.__format_filesystem()
713 def mount(self, options = None):
715 DiskMount.mount(self, options)
718 msger.debug("Checking filesystem %s" % self.disk.lofile)
719 runner.quiet([self.btrfsckcmd, self.disk.lofile])
721 def __get_size_from_filesystem(self):
722 return self.disk.size
724 def __resize_to_minimal(self):
727 return self.__get_size_from_filesystem()
729 def resparse(self, size = None):
731 minsize = self.__resize_to_minimal()
732 self.disk.truncate(minsize)
733 self.__resize_filesystem(size)
736 class DeviceMapperSnapshot(object):
737 def __init__(self, imgloop, cowloop):
738 self.imgloop = imgloop
739 self.cowloop = cowloop
741 self.__created = False
743 self.dmsetupcmd = find_binary_path("dmsetup")
745 """Load dm_snapshot if it isn't loaded"""
746 load_module("dm_snapshot")
749 if self.__name is None:
751 return os.path.join("/dev/mapper", self.__name)
752 path = property(get_path)
758 self.imgloop.create()
759 self.cowloop.create()
761 self.__name = "imgcreate-%d-%d" % (os.getpid(),
762 random.randint(0, 2**16))
764 size = os.stat(self.imgloop.lofile)[stat.ST_SIZE]
766 table = "0 %d snapshot %s %s p 8" % (size / 512,
770 args = [self.dmsetupcmd, "create", self.__name, "--table", table]
771 if runner.show(args) != 0:
772 self.cowloop.cleanup()
773 self.imgloop.cleanup()
774 raise SnapshotError("Could not create snapshot device using: " + ' '.join(args))
776 self.__created = True
778 def remove(self, ignore_errors = False):
779 if not self.__created:
783 rc = runner.show([self.dmsetupcmd, "remove", self.__name])
784 if not ignore_errors and rc != 0:
785 raise SnapshotError("Could not remove snapshot device")
788 self.__created = False
790 self.cowloop.cleanup()
791 self.imgloop.cleanup()
793 def get_cow_used(self):
794 if not self.__created:
798 # dmsetup status on a snapshot returns e.g.
799 # "0 8388608 snapshot 416/1048576"
800 # or, more generally:
802 # where C is the number of 512 byte sectors in use
804 out = runner.outs([self.dmsetupcmd, "status", self.__name])
806 return int((out.split()[3]).split('/')[0]) * 512
808 raise SnapshotError("Failed to parse dmsetup status: " + out)
810 def create_image_minimizer(path, image, minimal_size):
812 Builds a copy-on-write image which can be used to
813 create a device-mapper snapshot of an image where
814 the image's filesystem is as small as possible
817 1) Create a sparse COW
818 2) Loopback mount the image and the COW
819 3) Create a device-mapper snapshot of the image
821 4) Resize the filesystem to the minimal size
822 5) Determine the amount of space used in the COW
823 6) Restroy the device-mapper snapshot
824 7) Truncate the COW, removing unused space
825 8) Create a squashfs of the COW
827 imgloop = LoopbackDisk(image, None) # Passing bogus size - doesn't matter
829 cowloop = SparseLoopbackDisk(os.path.join(os.path.dirname(path), "osmin"),
832 snapshot = DeviceMapperSnapshot(imgloop, cowloop)
837 resize2fs(snapshot.path, minimal_size)
839 cow_used = snapshot.get_cow_used()
841 snapshot.remove(ignore_errors = (not sys.exc_info()[0] is None))
843 cowloop.truncate(cow_used)
845 mksquashfs(cowloop.lofile, path)
847 os.unlink(cowloop.lofile)
849 def load_module(module):
851 for line in open('/proc/modules').xreadlines():
852 if line.startswith("%s " % module):
856 msger.info("Loading %s..." % module)
857 runner.quiet(['modprobe', module])
859 def myurlgrab(url, filename, proxies, progress_obj = None):
860 from pykickstart.urlgrabber.grabber import URLGrabber, URLGrabError
863 if progress_obj is None:
864 progress_obj = TextProgress()
866 if url.startswith("file:/"):
867 file = url.replace("file:", "")
868 if not os.path.exists(file):
869 raise CreatorError("URLGrabber error: can't find file %s" % file)
870 runner.show(['cp', "-f", file, filename])
873 filename = g.urlgrab(url = url, filename = filename,
874 ssl_verify_host = False, ssl_verify_peer = False,
875 proxies = proxies, http_headers = (('Pragma', 'no-cache'),), progress_obj = progress_obj)
876 except URLGrabError, e:
877 raise CreatorError("URLGrabber error: %s" % url)
881 def get_loop_device(losetupcmd, lofile):
882 """ Get a lock to synchronize getting a loopback device """
884 # internal class for simple lock
885 class FileLock(object):
886 def __init__(self, filename):
887 self.filename = filename
891 atexit.register(self.release)
895 self.fd = os.open(self.filename, os.O_CREAT | os.O_EXCL)
903 if self.fd is not None:
905 os.remove(self.filename)
909 lock = FileLock("/var/lock/._mic_loopdev.lock")
911 while not lock.acquire():
913 raise MountError("Timeout! Failed to find a free loop device")
917 rc, losetupOutput = runner.runtool([losetupcmd, "-f"])
921 raise MountError("Failed to allocate loop device for '%s'" % lofile)
923 loopdev = losetupOutput.split()[0]
925 rc = runner.show([losetupcmd, loopdev, lofile])
929 raise MountError("Failed to allocate loop device for '%s'" % lofile)