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.
31 from mic.utils import runner
32 from mic.utils.errors import *
35 def find_binary_inchroot(binary, chroot):
43 bin_path = "%s/%s" % (path, binary)
44 if os.path.exists("%s/%s" % (chroot, bin_path)):
48 def find_binary_path(binary):
49 if "PATH" in os.environ:
50 paths = os.environ["PATH"].split(":")
53 if "HOME" in os.environ:
54 paths += [os.environ["HOME"] + "/bin"]
55 paths += ["/usr/local/sbin", "/usr/local/bin", "/usr/sbin", "/usr/bin", "/sbin", "/bin"]
58 bin_path = "%s/%s" % (path, binary)
59 if os.path.exists(bin_path):
61 raise CreatorError("Command '%s' is not available." % binary)
63 def makedirs(dirname):
64 """A version of os.makedirs() that doesn't throw an
65 exception if the leaf directory already exists.
69 except OSError as err:
70 if err.errno != errno.EEXIST:
73 def mkvdfs(in_img, out_img, fsoptions):
74 """ This function is incomplete. """
75 fullpathmkvdfs = find_binary_path("mkfs.vdfs")
76 # args = fullpathmkvdfs + " -i -r "+ in_img + " -z 1024M -s " + out_img
77 args = fullpathmkvdfs + " " + fsoptions + " -r " + in_img + " " + out_img
78 msger.verbose("vdfs args: %s" % args)
79 runner.show("%s --help" % fullpathmkvdfs)
80 # if not sys.stdout.isatty():
81 # args.append("-no-progress")
82 # runner.show("%s --help" % fullpathmkvdfs)
83 ret = runner.show(args)
85 runner.show ("vdfs error")
86 raise VdfsError("' %s' exited with error (%d)" % (args, ret))
88 def mksquashfs(in_img, out_img):
89 fullpathmksquashfs = find_binary_path("mksquashfs")
90 args = [fullpathmksquashfs, in_img, out_img]
92 if not sys.stdout.isatty():
93 args.append("-no-progress")
95 ret = runner.show(args)
97 raise SquashfsError("'%s' exited with error (%d)" % (' '.join(args), ret))
99 def resize2fs(fs, size):
100 resize2fs = find_binary_path("resize2fs")
102 # it means to minimalize it
103 return runner.show([resize2fs, '-M', fs])
105 return runner.show([resize2fs, fs, "%sK" % (size / 1024,)])
107 class BindChrootMount:
108 """Represents a bind mount of a directory into a chroot."""
109 def __init__(self, src, chroot, dest = None, option = None):
110 self.root = os.path.abspath(os.path.expanduser(chroot))
111 self.mount_option = option
113 self.orig_src = self.src = src
114 if os.path.islink(src):
115 self.src = os.readlink(src)
116 if not self.src.startswith('/'):
117 self.src = os.path.abspath(os.path.join(os.path.dirname(src),
122 self.dest = os.path.join(self.root, dest.lstrip('/'))
125 self.mountcmd = find_binary_path("mount")
126 self.umountcmd = find_binary_path("umount")
129 with open('/proc/mounts') as f:
131 if line.split()[1] == os.path.abspath(self.dest):
137 if self.mounted or self.ismounted():
142 except OSError as err:
143 if err.errno == errno.ENOSPC:
144 msger.warning("No space left on device '%s'" % err.filename)
147 if self.mount_option:
148 cmdline = [self.mountcmd, "-o" ,"bind", "-o", "%s" % \
149 self.mount_option, self.src, self.dest]
151 cmdline = [self.mountcmd, "-o" ,"bind", self.src, self.dest]
152 rc, errout = runner.runtool(cmdline, catch=2)
154 raise MountError("Bind-mounting '%s' to '%s' failed: %s" %
155 (self.src, self.dest, errout))
158 if os.path.islink(self.orig_src):
159 dest = os.path.join(self.root, self.orig_src.lstrip('/'))
160 if not os.path.exists(dest):
161 os.symlink(self.src, dest)
164 if self.mounted or self.ismounted():
165 runner.show([self.umountcmd, "-l", self.dest])
169 """LoopbackMount compatibility layer for old API"""
170 def __init__(self, lofile, mountdir, fstype = None):
171 self.diskmount = DiskMount(LoopbackDisk(lofile,size = 0),mountdir,fstype,rmmountdir = True)
173 self.losetupcmd = find_binary_path("losetup")
176 self.diskmount.cleanup()
179 self.diskmount.unmount()
183 runner.show([self.losetupcmd, "-d", self.loopdev])
191 self.loopdev = get_loop_device(self.losetupcmd, self.lofile)
195 self.diskmount.mount()
197 class SparseLoopbackMount(LoopbackMount):
198 """SparseLoopbackMount compatibility layer for old API"""
199 def __init__(self, lofile, mountdir, size, fstype = None):
200 self.diskmount = DiskMount(SparseLoopbackDisk(lofile,size),mountdir,fstype,rmmountdir = True)
202 def expand(self, create = False, size = None):
203 self.diskmount.disk.expand(create, size)
205 def truncate(self, size = None):
206 self.diskmount.disk.truncate(size)
209 self.diskmount.disk.create()
211 class SparseExtLoopbackMount(SparseLoopbackMount):
212 """SparseExtLoopbackMount compatibility layer for old API"""
213 def __init__(self, lofile, mountdir, size, fstype, blocksize, fslabel):
214 self.diskmount = ExtDiskMount(SparseLoopbackDisk(lofile,size), mountdir, fstype, blocksize, fslabel, rmmountdir = True)
217 def __format_filesystem(self):
218 self.diskmount.__format_filesystem()
221 self.diskmount.disk.create()
223 def resize(self, size = None):
224 return self.diskmount.__resize_filesystem(size)
227 self.diskmount.mount()
230 self.extdiskmount.__fsck()
232 def __get_size_from_filesystem(self):
233 return self.diskmount.__get_size_from_filesystem()
235 def __resize_to_minimal(self):
236 return self.diskmount.__resize_to_minimal()
238 def resparse(self, size = None):
239 return self.diskmount.resparse(size)
242 """Generic base object for a disk
244 The 'create' method must make the disk visible as a block device - eg
245 by calling losetup. For RawDisk, this is obviously a no-op. The 'cleanup'
246 method must undo the 'create' operation.
248 def __init__(self, size, device = None):
249 self._device = device
258 def get_device(self):
260 def set_device(self, path):
262 device = property(get_device, set_device)
266 size = property(get_size)
270 """A Disk backed by a block device.
271 Note that create() is a no-op.
273 def __init__(self, size, device):
274 Disk.__init__(self, size, device)
282 class LoopbackDisk(Disk):
283 """A Disk backed by a file via the loop module."""
284 def __init__(self, lofile, size):
285 Disk.__init__(self, size)
287 self.losetupcmd = find_binary_path("losetup")
293 return os.path.exists(self.lofile)
296 if self.device is not None:
299 self.device = get_loop_device(self.losetupcmd, self.lofile)
302 if self.device is None:
304 msger.debug("Losetup remove %s" % self.device)
305 rc = runner.show([self.losetupcmd, "-d", self.device])
308 def reread_size(self):
309 if self.device is None:
311 msger.debug("Reread size %s" % self.device)
312 rc = runner.show([self.losetupcmd, "-c", self.device])
313 # XXX: (WORKAROUND) re-setup loop device when losetup isn't support '-c' option.
315 msger.debug("Fail to reread size, Try to re-setup loop device to reread the size of the file associated")
316 runner.show([self.losetupcmd, "-d", self.device])
317 runner.show([self.losetupcmd, self.device, self.lofile])
319 class SparseLoopbackDisk(LoopbackDisk):
320 """A Disk backed by a sparse file via the loop module."""
321 def __init__(self, lofile, size):
322 LoopbackDisk.__init__(self, lofile, size)
324 def expand(self, create = False, size = None):
328 if not os.path.exists(self.lofile):
329 makedirs(os.path.dirname(self.lofile))
334 msger.debug("Extending sparse file %s to %d" % (self.lofile, size))
336 fd = os.open(self.lofile, flags, 0o644)
338 fd = os.open(self.lofile, flags)
343 os.ftruncate(fd, size)
345 # may be limited by 2G in 32bit env
346 os.ftruncate(fd, 2**31)
350 def truncate(self, size = None):
354 msger.debug("Truncating sparse file %s to %d" % (self.lofile, size))
355 fd = os.open(self.lofile, os.O_WRONLY)
356 os.ftruncate(fd, size)
360 if self.device is not None:
363 self.expand(create = True)
364 LoopbackDisk.create(self)
367 """A generic base class to deal with mounting things."""
368 def __init__(self, mountdir):
369 self.mountdir = mountdir
374 def mount(self, options = None):
380 class DiskMount(Mount):
381 """A Mount object that handles mounting of a Disk."""
382 def __init__(self, disk, mountdir, fstype = None, rmmountdir = True):
383 Mount.__init__(self, mountdir)
387 self.rmmountdir = rmmountdir
392 self.mkfscmd = find_binary_path("mkfs." + self.fstype)
395 self.mountcmd = find_binary_path("mount")
396 self.umountcmd = find_binary_path("umount")
404 msger.debug("Unmounting directory %s" % self.mountdir)
405 runner.quiet('sync') # sync the data on this mount point
406 rc = runner.show([self.umountcmd, "-l", self.mountdir])
410 raise MountError("Failed to umount %s" % self.mountdir)
411 if self.rmdir and not self.mounted:
413 os.rmdir(self.mountdir)
423 def mount(self, options = None):
427 if not os.path.isdir(self.mountdir):
428 msger.debug("Creating mount point %s" % self.mountdir)
429 os.makedirs(self.mountdir)
430 self.rmdir = self.rmmountdir
434 msger.debug("Mounting %s at %s" % (self.disk.device, self.mountdir))
436 args = [ self.mountcmd, "-o", options, self.disk.device, self.mountdir ]
438 args = [ self.mountcmd, self.disk.device, self.mountdir ]
440 args.extend(["-t", self.fstype])
442 rc = runner.show(args)
444 raise MountError("Failed to mount '%s' to '%s' with command '%s'. Retval: %s" %
445 (self.disk.device, self.mountdir, " ".join(args), rc))
449 class F2fsDiskMount(DiskMount):
450 """A DiskMount object that is able to format/resize f2fs filesystems."""
451 def __init__(self, disk, mountdir, fstype, blocksize, fslabel, rmmountdir=True, skipformat = False, fsopts = None, fsuuid=None):
453 DiskMount.__init__(self, disk, mountdir, fstype, rmmountdir)
454 self.blocksize = blocksize
455 self.fslabel = fslabel.replace("/", "")
456 self.uuid = fsuuid or None
457 self.skipformat = skipformat
459 self.__f2fsopts = None
460 self.blkidcmd = find_binary_path("blkid")
461 self.dumpe2fs = find_binary_path("dump." + self.fstype)
462 self.fsckcmd = find_binary_path("fsck." + self.fstype)
463 self.resizecmd = find_binary_path("resize." + self.fstype)
465 def __get_f2fsopts(self):
466 return self.__f2f2opts
468 def __set_f2fsopts(self, val):
470 self.__f2fsopts = None
472 self.__f2fsopts = val
473 f2fsopts = property(__get_f2fsopts, __set_f2fsopts)
475 def __check_f2fs(self):
477 """ Need to load f2fs module to mount it """
479 for line in open("/proc/filesystems"):
480 if line.find("f2fs") > -1:
484 raise MountError("Your system can't mount f2fs filesystem, please make sure your kernel has f2fs support and the module f2fs.ko has been loaded.")
486 def __parse_field(self, output, field):
487 for line in output.split(" "):
488 if line.startswith(field + "="):
489 return line[len(field) + 1:].strip().replace("\"", "")
491 raise KeyError("Failed to find field '%s' in output" % field)
493 def __format_filesystem(self):
495 msger.debug("Skip filesystem format.")
498 msger.verbose("Formating %s filesystem on %s" % (self.fstype, self.disk.device))
500 cmdlist = [self.mkfscmd, "-l", self.fslabel]
502 cmdlist.extend(self._f2fsopts.split())
503 cmdlist.extend([self.disk.device])
505 rc, errout = runner.runtool(cmdlist, catch=2)
507 raise MountError("Error creating %s filesystem on disk %s:\n%s" %
508 (self.fstype, self.disk.device, errout))
510 self.uuid = self.__parse_field(runner.outs([self.blkidcmd, self.disk.device]), "UUID")
512 def __resize_filesystem(self, size = None):
513 msger.info("Resizing filesystem ...")
514 current_size = os.stat(self.disk.lofile)[stat.ST_SIZE]
517 size = self.disk.size
519 if size == current_size:
522 if size > current_size:
523 self.disk.expand(size=size)
531 if not self.disk.fixed() and self.disk.exists():
537 self.__resize_filesystem()
539 self.__format_filesystem()
541 def mount(self, options = None, init_expand = False):
544 expand_size = int(self.disk.size * 1.5)
545 msger.info("Initial partition size expanded : %ld -> %ld" % (self.disk.size, expand_size))
546 self.__resize_filesystem(expand_size)
547 self.disk.reread_size()
548 DiskMount.mount(self, options)
551 msger.info("Checking filesystem %s" % self.disk.lofile)
552 runner.quiet([self.fsckcmd, self.disk.lofile])
554 def __get_size_from_filesystem(self):
555 return self.disk.size
557 def __resize_to_minimal(self):
558 msger.info("Resizing filesystem to minimal ...")
561 return self.__get_size_from_filesystem()
563 def resparse(self, size = None):
568 minsize = self.__resize_to_minimal()
569 self.disk.truncate(minsize)
570 self.__resize_filesystem(size)
573 class ExtDiskMount(DiskMount):
574 """A DiskMount object that is able to format/resize ext[23] filesystems."""
575 def __init__(self, disk, mountdir, fstype, blocksize, fslabel, rmmountdir=True, skipformat = False, fsopts = None, fsuuid=None):
576 DiskMount.__init__(self, disk, mountdir, fstype, rmmountdir)
577 self.blocksize = blocksize
578 self.fslabel = fslabel.replace("/", "")
579 self.uuid = fsuuid or str(uuid.uuid4())
580 self.skipformat = skipformat
582 self.__extopts = None
583 self.dumpe2fs = find_binary_path("dumpe2fs")
584 self.tune2fs = find_binary_path("tune2fs")
586 def __get_extopts(self):
587 return self.__extopts
589 def __set_extopts(self, val):
591 self.__extopts = None
593 m = re.search(r'-b\s*(?P<blocksize>\d+)', val)
595 self.blocksize = int(m.group('blocksize'))
596 val = val.replace(m.group(), '')
599 extopts = property(__get_extopts, __set_extopts)
601 def __parse_field(self, output, field):
602 for line in output.split("\n"):
603 if line.startswith(field + ":"):
604 return line[len(field) + 1:].strip()
606 raise KeyError("Failed to find field '%s' in output" % field)
608 def __format_filesystem(self):
610 msger.debug("Skip filesystem format.")
613 msger.verbose("Formating %s filesystem on %s" % (self.fstype, self.disk.device))
614 cmdlist = [self.mkfscmd, "-F", "-L", self.fslabel, "-m", "1", "-b",
615 str(self.blocksize), "-U", self.uuid]
617 cmdlist.extend(self.__extopts.split())
618 cmdlist.extend([self.disk.device])
620 rc, errout = runner.runtool(cmdlist, catch=2)
622 raise MountError("Error creating %s filesystem on disk %s:\n%s" %
623 (self.fstype, self.disk.device, errout))
625 if not self.__extopts:
626 msger.debug("Tuning filesystem on %s" % self.disk.device)
627 runner.show([self.tune2fs, "-c0", "-i0", "-Odir_index", "-ouser_xattr,acl", self.disk.device])
629 def __resize_filesystem(self, size = None):
630 msger.info("Resizing filesystem ...")
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=size)
644 resize2fs(self.disk.lofile, size)
645 if size and size != os.stat(self.disk.lofile)[stat.ST_SIZE]:
646 raise MountError("Failed to resize filesystem %s to %d " % (self.disk.lofile, size))
652 if not self.disk.fixed() and self.disk.exists():
658 self.__resize_filesystem()
660 self.__format_filesystem()
662 def mount(self, options = None, init_expand = False):
665 expand_size = int(self.disk.size * 1.5)
666 msger.info("Initial partition size expanded : %ld -> %ld" % (self.disk.size, expand_size))
667 self.__resize_filesystem(expand_size)
668 self.disk.reread_size()
669 DiskMount.mount(self, options)
672 msger.info("Checking filesystem %s" % self.disk.lofile)
673 runner.quiet(["/sbin/e2fsck", "-f", "-y", self.disk.lofile])
675 def __get_size_from_filesystem(self):
676 return int(self.__parse_field(runner.outs([self.dumpe2fs, '-h', self.disk.lofile]),
677 "Block count")) * self.blocksize
679 def __resize_to_minimal(self):
680 msger.info("Resizing filesystem to minimal ...")
682 resize2fs(self.disk.lofile, 0)
683 return self.__get_size_from_filesystem()
685 def resparse(self, size = None):
690 minsize = self.__resize_to_minimal()
691 self.disk.truncate(minsize)
693 self.__resize_filesystem(size)
696 class VfatDiskMount(DiskMount):
697 """A DiskMount object that is able to format vfat/msdos filesystems."""
698 def __init__(self, disk, mountdir, fstype, blocksize, fslabel, rmmountdir=True, skipformat = False, fsopts = None, fsuuid = None):
699 DiskMount.__init__(self, disk, mountdir, fstype, rmmountdir)
700 self.blocksize = blocksize
701 self.fslabel = fslabel.replace("/", "")
702 rand1 = random.randint(0, 2**16 - 1)
703 rand2 = random.randint(0, 2**16 - 1)
704 self.uuid = fsuuid or "%04X-%04X" % (rand1, rand2)
705 self.skipformat = skipformat
707 self.fsckcmd = find_binary_path("fsck." + self.fstype)
709 def __format_filesystem(self):
711 msger.debug("Skip filesystem format.")
714 msger.verbose("Formating %s filesystem on %s" % (self.fstype, self.disk.device))
715 rc = runner.show([self.mkfscmd, "-n", self.fslabel,
716 "-i", self.uuid.replace("-", ""), self.disk.device])
718 raise MountError("Error creating %s filesystem on disk %s" % (self.fstype,self.disk.device))
720 msger.verbose("Tuning filesystem on %s" % self.disk.device)
722 def __resize_filesystem(self, size = None):
723 current_size = os.stat(self.disk.lofile)[stat.ST_SIZE]
726 size = self.disk.size
728 if size == current_size:
731 if size > current_size:
732 self.disk.expand(size=size)
736 #resize2fs(self.disk.lofile, size)
741 if not self.disk.fixed() and self.disk.exists():
747 self.__resize_filesystem()
749 self.__format_filesystem()
751 def mount(self, options = None, init_expand = False):
754 expand_size = int(self.disk.size * 1.5)
755 msger.info("Initial partition size expanded : %ld -> %ld" % (self.disk.size, expand_size))
756 self.__resize_filesystem(expand_size)
757 self.disk.reread_size()
758 DiskMount.mount(self, options)
761 msger.debug("Checking filesystem %s" % self.disk.lofile)
762 runner.show([self.fsckcmd, "-y", self.disk.lofile])
764 def __get_size_from_filesystem(self):
765 return self.disk.size
767 def __resize_to_minimal(self):
771 # Use a binary search to find the minimal size
772 # we can resize the image to
775 top = self.__get_size_from_filesystem()
778 def resparse(self, size = None):
780 minsize = self.__resize_to_minimal()
781 self.disk.truncate(minsize)
782 self.__resize_filesystem(size)
785 class BtrfsDiskMount(DiskMount):
786 """A DiskMount object that is able to format/resize btrfs filesystems."""
787 def __init__(self, disk, mountdir, fstype, blocksize, fslabel, rmmountdir=True, skipformat = False, fsopts = None, fsuuid = None):
789 DiskMount.__init__(self, disk, mountdir, fstype, rmmountdir)
790 self.blocksize = blocksize
791 self.fslabel = fslabel.replace("/", "")
792 self.uuid = fsuuid or None
793 self.skipformat = skipformat
795 self.blkidcmd = find_binary_path("blkid")
796 self.btrfsckcmd = find_binary_path("btrfsck")
798 def __check_btrfs(self):
800 """ Need to load btrfs module to mount it """
802 for line in open("/proc/filesystems"):
803 if line.find("btrfs") > -1:
807 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.")
809 # disable selinux, selinux will block write
810 if os.path.exists("/usr/sbin/setenforce"):
811 runner.show(["/usr/sbin/setenforce", "0"])
813 def __parse_field(self, output, field):
814 for line in output.split(" "):
815 if line.startswith(field + "="):
816 return line[len(field) + 1:].strip().replace("\"", "")
818 raise KeyError("Failed to find field '%s' in output" % field)
820 def __format_filesystem(self):
822 msger.debug("Skip filesystem format.")
825 msger.verbose("Formating %s filesystem on %s" % (self.fstype, self.disk.device))
826 rc = runner.show([self.mkfscmd, "-L", self.fslabel, "-m", "single", self.disk.device])
828 raise MountError("Error creating %s filesystem on disk %s" % (self.fstype,self.disk.device))
830 self.uuid = self.__parse_field(runner.outs([self.blkidcmd, self.disk.device]), "UUID")
832 def __resize_filesystem(self, size = None):
833 current_size = os.stat(self.disk.lofile)[stat.ST_SIZE]
836 size = self.disk.size
838 if size == current_size:
841 if size > current_size:
842 self.disk.expand(size=size)
849 if not self.disk.fixed() and self.disk.exists():
855 self.__resize_filesystem()
857 self.__format_filesystem()
859 def mount(self, options = None, init_expand = False):
862 expand_size = int(self.disk.size * 1.5)
863 msger.info("Initial partition size expanded : %ld -> %ld" % (self.disk.size, expand_size))
864 self.__resize_filesystem(expand_size)
865 self.disk.reread_size()
866 DiskMount.mount(self, options)
869 msger.debug("Checking filesystem %s" % self.disk.lofile)
870 runner.quiet([self.btrfsckcmd, self.disk.lofile])
872 def __get_size_from_filesystem(self):
873 return self.disk.size
875 def __resize_to_minimal(self):
878 return self.__get_size_from_filesystem()
880 def resparse(self, size = None):
882 minsize = self.__resize_to_minimal()
883 self.disk.truncate(minsize)
884 self.__resize_filesystem(size)
887 class DeviceMapperSnapshot(object):
888 def __init__(self, imgloop, cowloop):
889 self.imgloop = imgloop
890 self.cowloop = cowloop
892 self.__created = False
894 self.dmsetupcmd = find_binary_path("dmsetup")
896 """Load dm_snapshot if it isn't loaded"""
897 load_module("dm_snapshot")
900 if self.__name is None:
902 return os.path.join("/dev/mapper", self.__name)
903 path = property(get_path)
909 self.imgloop.create()
910 self.cowloop.create()
912 self.__name = "imgcreate-%d-%d" % (os.getpid(),
913 random.randint(0, 2**16))
915 size = os.stat(self.imgloop.lofile)[stat.ST_SIZE]
917 table = "0 %d snapshot %s %s p 8" % (size / 512,
921 args = [self.dmsetupcmd, "create", self.__name, "--table", table]
922 if runner.show(args) != 0:
923 self.cowloop.cleanup()
924 self.imgloop.cleanup()
925 raise SnapshotError("Could not create snapshot device using: " + ' '.join(args))
927 self.__created = True
929 def remove(self, ignore_errors = False):
930 if not self.__created:
934 rc = runner.show([self.dmsetupcmd, "remove", self.__name])
935 if not ignore_errors and rc != 0:
936 raise SnapshotError("Could not remove snapshot device")
939 self.__created = False
941 self.cowloop.cleanup()
942 self.imgloop.cleanup()
944 def get_cow_used(self):
945 if not self.__created:
949 # dmsetup status on a snapshot returns e.g.
950 # "0 8388608 snapshot 416/1048576"
951 # or, more generally:
953 # where C is the number of 512 byte sectors in use
955 out = runner.outs([self.dmsetupcmd, "status", self.__name])
957 return int((out.split()[3]).split('/')[0]) * 512
959 raise SnapshotError("Failed to parse dmsetup status: " + out)
961 def create_image_minimizer(path, image, minimal_size):
963 Builds a copy-on-write image which can be used to
964 create a device-mapper snapshot of an image where
965 the image's filesystem is as small as possible
968 1) Create a sparse COW
969 2) Loopback mount the image and the COW
970 3) Create a device-mapper snapshot of the image
972 4) Resize the filesystem to the minimal size
973 5) Determine the amount of space used in the COW
974 6) Restroy the device-mapper snapshot
975 7) Truncate the COW, removing unused space
976 8) Create a squashfs of the COW
978 imgloop = LoopbackDisk(image, None) # Passing bogus size - doesn't matter
980 cowloop = SparseLoopbackDisk(os.path.join(os.path.dirname(path), "osmin"),
983 snapshot = DeviceMapperSnapshot(imgloop, cowloop)
988 resize2fs(snapshot.path, minimal_size)
990 cow_used = snapshot.get_cow_used()
992 snapshot.remove(ignore_errors = (not sys.exc_info()[0] is None))
994 cowloop.truncate(cow_used)
996 mksquashfs(cowloop.lofile, path)
998 os.unlink(cowloop.lofile)
1000 def load_module(module):
1002 for line in open('/proc/modules'):
1003 if line.startswith("%s " % module):
1007 msger.info("Loading %s..." % module)
1008 runner.quiet(['modprobe', module])
1010 class LoopDevice(object):
1011 def __init__(self, loopid=None):
1013 self.loopid = loopid
1014 self.created = False
1015 self.kpartxcmd = find_binary_path("kpartx")
1016 self.losetupcmd = find_binary_path("losetup")
1018 def register(self, device):
1019 self.device = device
1021 #self.created = True
1023 def reg_atexit(self):
1025 atexit.register(self.close)
1027 def _genloopid(self):
1029 if not glob.glob("/dev/loop[0-9]*"):
1032 fint = lambda x: x[9:].isdigit() and int(x[9:]) or 0
1033 maxid = 1 + max([x for x in map(fint, glob.glob("/dev/loop[0-9]*")) if x<256])
1034 if maxid < 10: maxid = 10
1036 raise Exception("maxid >= 256")
1039 def _kpseek(self, device):
1040 rc, out = runner.runtool([self.kpartxcmd, '-l', '-v', device])
1042 raise MountError("Can't query dm snapshot on %s" % device)
1043 for line in out.splitlines():
1044 if line and line.startswith("loop"):
1048 def _loseek(self, device):
1050 rc, out = runner.runtool([self.losetupcmd, '-a'])
1052 raise MountError("Failed to run 'losetup -a'")
1053 for line in out.splitlines():
1054 m = re.match("([^:]+): .*", line)
1055 if m and m.group(1) == device:
1060 if not self.created:
1062 self.loopid = self._genloopid()
1063 self.device = "/dev/loop%d" % self.loopid
1064 if os.path.exists(self.device):
1065 if self._loseek(self.device):
1066 raise MountError("Device busy: %s" % self.device)
1071 mknod = find_binary_path('mknod')
1072 rc = runner.show([mknod, '-m664', self.device, 'b', '7', str(self.loopid)])
1074 raise MountError("Failed to create device %s" % self.device)
1083 except MountError as e:
1084 raise CreatorError("%s" % e)
1088 if self.device is None:
1092 if self._kpseek(self.device):
1093 runner.quiet([self.kpartxcmd, "-d", self.device])
1094 if self._loseek(self.device):
1095 runner.quiet([self.losetupcmd, "-d", self.device])
1096 # FIXME: should sleep a while between two loseek
1097 if self._loseek(self.device):
1098 msger.warning("Can't cleanup loop device %s" % self.device)
1100 os.unlink(self.device)
1102 DEVICE_PIDFILE_DIR = "/var/tmp/mic/device"
1103 DEVICE_LOCKFILE = "/var/lock/__mic_loopdev.lock"
1105 def get_loop_device(losetupcmd, lofile):
1107 makedirs(os.path.dirname(DEVICE_LOCKFILE))
1108 fp = open(DEVICE_LOCKFILE, 'w')
1109 fcntl.flock(fp, fcntl.LOCK_EX)
1112 devinst = LoopDevice()
1114 # clean up left loop device first
1115 clean_loop_devices()
1117 # provide an avaible loop device
1118 rc, out = runner.runtool([losetupcmd, "-f"])
1120 loopdev = out.split()[0]
1121 devinst.register(loopdev)
1122 if not loopdev or not os.path.exists(loopdev):
1124 loopdev = devinst.device
1126 # setup a loop device for image file
1127 rc = runner.show([losetupcmd, loopdev, lofile])
1129 raise MountError("Failed to setup loop device for '%s'" % lofile)
1131 devinst.reg_atexit()
1133 # try to save device and pid
1134 makedirs(DEVICE_PIDFILE_DIR)
1135 pidfile = os.path.join(DEVICE_PIDFILE_DIR, os.path.basename(loopdev))
1136 if os.path.exists(pidfile):
1138 with open(pidfile, 'w') as wf:
1139 wf.write(str(os.getpid()))
1141 except MountError as err:
1142 raise CreatorError("%s" % str(err))
1147 fcntl.flock(fp, fcntl.LOCK_UN)
1154 def clean_loop_devices(piddir=DEVICE_PIDFILE_DIR):
1155 if not os.path.exists(piddir) or not os.path.isdir(piddir):
1158 for loopdev in os.listdir(piddir):
1159 pidfile = os.path.join(piddir, loopdev)
1161 with open(pidfile, 'r') as rf:
1162 devpid = int(rf.read())
1166 # if the process using this device is alive, skip it
1167 if not devpid or os.path.exists(os.path.join('/proc', str(devpid))):
1170 # try to clean it up
1172 devinst = LoopDevice()
1173 devinst.register(os.path.join('/dev', loopdev))