a5599c6636289268f4b203fb9f37361581b5eb1f
[tools/mic.git] / mic / utils / fs_related.py
1 #!/usr/bin/python -tt
2 #
3 # Copyright (c) 2007, Red Hat, Inc.
4 # Copyright (c) 2009, 2010, 2011 Intel, Inc.
5 #
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
9 #
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
13 # for more details.
14 #
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.
18
19 from __future__ import with_statement
20 import os
21 import sys
22 import errno
23 import stat
24 import random
25 import string
26 import time
27 import uuid
28
29 from mic import msger
30 from mic.utils import runner
31 from mic.utils.errors import *
32
33
34 def find_binary_inchroot(binary, chroot):
35     paths = ["/usr/sbin",
36              "/usr/bin",
37              "/sbin",
38              "/bin"
39             ]
40
41     for path in paths:
42         bin_path = "%s/%s" % (path, binary)
43         if os.path.exists("%s/%s" % (chroot, bin_path)):
44             return bin_path
45     return None
46
47 def find_binary_path(binary):
48     if os.environ.has_key("PATH"):
49         paths = os.environ["PATH"].split(":")
50     else:
51         paths = []
52         if os.environ.has_key("HOME"):
53             paths += [os.environ["HOME"] + "/bin"]
54         paths += ["/usr/local/sbin", "/usr/local/bin", "/usr/sbin", "/usr/bin", "/sbin", "/bin"]
55
56     for path in paths:
57         bin_path = "%s/%s" % (path, binary)
58         if os.path.exists(bin_path):
59             return bin_path
60     raise CreatorError("Command '%s' is not available." % binary)
61
62 def makedirs(dirname):
63     """A version of os.makedirs() that doesn't throw an
64     exception if the leaf directory already exists.
65     """
66     try:
67         os.makedirs(dirname)
68     except OSError, err:
69         if err.errno != errno.EEXIST:
70             raise
71
72 def mksquashfs(in_img, out_img):
73     fullpathmksquashfs = find_binary_path("mksquashfs")
74     args = [fullpathmksquashfs, in_img, out_img]
75
76     if not sys.stdout.isatty():
77         args.append("-no-progress")
78
79     ret = runner.show(args)
80     if ret != 0:
81         raise SquashfsError("'%s' exited with error (%d)" % (' '.join(args), ret))
82
83 def resize2fs(fs, size):
84     resize2fs = find_binary_path("resize2fs")
85     if size == 0:
86         # it means to minimalize it
87         return runner.show([resize2fs, '-M', fs])
88     else:
89         return runner.show([resize2fs, fs, "%sK" % (size / 1024,)])
90
91 def my_fuser(fp):
92     fuser = find_binary_path("fuser")
93     if not os.path.exists(fp):
94         return False
95
96     rc = runner.quiet([fuser, "-s", fp])
97     if rc == 0:
98         for pid in runner.outs([fuser, fp]).split():
99             fd = open("/proc/%s/cmdline" % pid, "r")
100             cmdline = fd.read()
101             fd.close()
102             if cmdline[:-1] == "/bin/bash":
103                 return True
104
105     # not found
106     return False
107
108 class BindChrootMount:
109     """Represents a bind mount of a directory into a chroot."""
110     def __init__(self, src, chroot, dest = None, option = None):
111         self.root = os.path.abspath(os.path.expanduser(chroot))
112         self.option = option
113
114         self.orig_src = self.src = src
115         if os.path.islink(src):
116             self.src = os.readlink(src)
117             if not self.src.startswith('/'):
118                 self.src = os.path.abspath(os.path.join(os.path.dirname(src),
119                                                         self.src))
120
121         if not dest:
122             dest = self.src
123         self.dest = os.path.join(self.root, dest.lstrip('/'))
124
125         self.mounted = False
126         self.mountcmd = find_binary_path("mount")
127         self.umountcmd = find_binary_path("umount")
128
129     def ismounted(self):
130         with open('/proc/mounts') as f:
131             for line in f:
132                 if line.split()[1] == os.path.abspath(self.dest):
133                     return True
134
135         return False
136
137     def has_chroot_instance(self):
138         lock = os.path.join(self.root, ".chroot.lock")
139         return my_fuser(lock)
140
141     def mount(self):
142         if self.mounted or self.ismounted():
143             return
144
145         makedirs(self.dest)
146         rc = runner.show([self.mountcmd, "--bind", self.src, self.dest])
147         if rc != 0:
148             raise MountError("Bind-mounting '%s' to '%s' failed" %
149                              (self.src, self.dest))
150         if self.option:
151             rc = runner.show([self.mountcmd, "--bind", "-o", "remount,%s" % self.option, self.dest])
152             if rc != 0:
153                 raise MountError("Bind-remounting '%s' failed" % self.dest)
154
155         self.mounted = True
156         if os.path.islink(self.orig_src):
157             dest = os.path.join(self.root, self.orig_src.lstrip('/'))
158             if not os.path.exists(dest):
159                 os.symlink(self.src, dest)
160
161     def unmount(self):
162         if self.has_chroot_instance():
163             return
164
165         if self.ismounted():
166             runner.show([self.umountcmd, "-l", self.dest])
167         self.mounted = False
168
169 class LoopbackMount:
170     """LoopbackMount  compatibility layer for old API"""
171     def __init__(self, lofile, mountdir, fstype = None):
172         self.diskmount = DiskMount(LoopbackDisk(lofile,size = 0),mountdir,fstype,rmmountdir = True)
173         self.losetup = False
174         self.losetupcmd = find_binary_path("losetup")
175
176     def cleanup(self):
177         self.diskmount.cleanup()
178
179     def unmount(self):
180         self.diskmount.unmount()
181
182     def lounsetup(self):
183         if self.losetup:
184             runner.show([self.losetupcmd, "-d", self.loopdev])
185             self.losetup = False
186             self.loopdev = None
187
188     def loopsetup(self):
189         if self.losetup:
190             return
191
192         self.loopdev = get_loop_device(self.losetupcmd, self.lofile)
193         self.losetup = True
194
195     def mount(self):
196         self.diskmount.mount()
197
198 class SparseLoopbackMount(LoopbackMount):
199     """SparseLoopbackMount  compatibility layer for old API"""
200     def __init__(self, lofile, mountdir, size, fstype = None):
201         self.diskmount = DiskMount(SparseLoopbackDisk(lofile,size),mountdir,fstype,rmmountdir = True)
202
203     def expand(self, create = False, size = None):
204         self.diskmount.disk.expand(create, size)
205
206     def truncate(self, size = None):
207         self.diskmount.disk.truncate(size)
208
209     def create(self):
210         self.diskmount.disk.create()
211
212 class SparseExtLoopbackMount(SparseLoopbackMount):
213     """SparseExtLoopbackMount  compatibility layer for old API"""
214     def __init__(self, lofile, mountdir, size, fstype, blocksize, fslabel):
215         self.diskmount = ExtDiskMount(SparseLoopbackDisk(lofile,size), mountdir, fstype, blocksize, fslabel, rmmountdir = True)
216
217
218     def __format_filesystem(self):
219         self.diskmount.__format_filesystem()
220
221     def create(self):
222         self.diskmount.disk.create()
223
224     def resize(self, size = None):
225         return self.diskmount.__resize_filesystem(size)
226
227     def mount(self):
228         self.diskmount.mount()
229
230     def __fsck(self):
231         self.extdiskmount.__fsck()
232
233     def __get_size_from_filesystem(self):
234         return self.diskmount.__get_size_from_filesystem()
235
236     def __resize_to_minimal(self):
237         return self.diskmount.__resize_to_minimal()
238
239     def resparse(self, size = None):
240         return self.diskmount.resparse(size)
241
242 class Disk:
243     """Generic base object for a disk
244
245     The 'create' method must make the disk visible as a block device - eg
246     by calling losetup. For RawDisk, this is obviously a no-op. The 'cleanup'
247     method must undo the 'create' operation.
248     """
249     def __init__(self, size, device = None):
250         self._device = device
251         self._size = size
252
253     def create(self):
254         pass
255
256     def cleanup(self):
257         pass
258
259     def get_device(self):
260         return self._device
261     def set_device(self, path):
262         self._device = path
263     device = property(get_device, set_device)
264
265     def get_size(self):
266         return self._size
267     size = property(get_size)
268
269
270 class RawDisk(Disk):
271     """A Disk backed by a block device.
272     Note that create() is a no-op.
273     """
274     def __init__(self, size, device):
275         Disk.__init__(self, size, device)
276
277     def fixed(self):
278         return True
279
280     def exists(self):
281         return True
282
283 class LoopbackDisk(Disk):
284     """A Disk backed by a file via the loop module."""
285     def __init__(self, lofile, size):
286         Disk.__init__(self, size)
287         self.lofile = lofile
288         self.losetupcmd = find_binary_path("losetup")
289
290     def fixed(self):
291         return False
292
293     def exists(self):
294         return os.path.exists(self.lofile)
295
296     def create(self):
297         if self.device is not None:
298             return
299
300         self.device = get_loop_device(self.losetupcmd, self.lofile)
301
302     def cleanup(self):
303         if self.device is None:
304             return
305         msger.debug("Losetup remove %s" % self.device)
306         rc = runner.show([self.losetupcmd, "-d", self.device])
307         self.device = None
308
309 class SparseLoopbackDisk(LoopbackDisk):
310     """A Disk backed by a sparse file via the loop module."""
311     def __init__(self, lofile, size):
312         LoopbackDisk.__init__(self, lofile, size)
313
314     def expand(self, create = False, size = None):
315         flags = os.O_WRONLY
316         if create:
317             flags |= os.O_CREAT
318             if not os.path.exists(self.lofile):
319                 makedirs(os.path.dirname(self.lofile))
320
321         if size is None:
322             size = self.size
323
324         msger.debug("Extending sparse file %s to %d" % (self.lofile, size))
325         if create:
326             fd = os.open(self.lofile, flags, 0644)
327         else:
328             fd = os.open(self.lofile, flags)
329
330         if size <= 0:
331             size = 1
332         try:
333             os.ftruncate(fd, size)
334         except:
335             # may be limited by 2G in 32bit env
336             os.ftruncate(fd, 2**31L)
337
338         os.close(fd)
339
340     def truncate(self, size = None):
341         if size is None:
342             size = self.size
343
344         msger.debug("Truncating sparse file %s to %d" % (self.lofile, size))
345         fd = os.open(self.lofile, os.O_WRONLY)
346         os.ftruncate(fd, size)
347         os.close(fd)
348
349     def create(self):
350         self.expand(create = True)
351         LoopbackDisk.create(self)
352
353 class Mount:
354     """A generic base class to deal with mounting things."""
355     def __init__(self, mountdir):
356         self.mountdir = mountdir
357
358     def cleanup(self):
359         self.unmount()
360
361     def mount(self, options = None):
362         pass
363
364     def unmount(self):
365         pass
366
367 class DiskMount(Mount):
368     """A Mount object that handles mounting of a Disk."""
369     def __init__(self, disk, mountdir, fstype = None, rmmountdir = True):
370         Mount.__init__(self, mountdir)
371
372         self.disk = disk
373         self.fstype = fstype
374         self.rmmountdir = rmmountdir
375
376         self.mounted = False
377         self.rmdir   = False
378         if fstype:
379             self.mkfscmd = find_binary_path("mkfs." + self.fstype)
380         else:
381             self.mkfscmd = None
382         self.mountcmd = find_binary_path("mount")
383         self.umountcmd = find_binary_path("umount")
384
385     def cleanup(self):
386         Mount.cleanup(self)
387         self.disk.cleanup()
388
389     def unmount(self):
390         if self.mounted:
391             msger.debug("Unmounting directory %s" % self.mountdir)
392             runner.quiet('sync') # sync the data on this mount point
393             rc = runner.show([self.umountcmd, "-l", self.mountdir])
394             if rc == 0:
395                 self.mounted = False
396             else:
397                 raise MountError("Failed to umount %s" % self.mountdir)
398         if self.rmdir and not self.mounted:
399             try:
400                 os.rmdir(self.mountdir)
401             except OSError, e:
402                 pass
403             self.rmdir = False
404
405
406     def __create(self):
407         self.disk.create()
408
409
410     def mount(self, options = None):
411         if self.mounted:
412             return
413
414         if not os.path.isdir(self.mountdir):
415             msger.debug("Creating mount point %s" % self.mountdir)
416             os.makedirs(self.mountdir)
417             self.rmdir = self.rmmountdir
418
419         self.__create()
420
421         msger.debug("Mounting %s at %s" % (self.disk.device, self.mountdir))
422         if options:
423             args = [ self.mountcmd, "-o", options, self.disk.device, self.mountdir ]
424         else:
425             args = [ self.mountcmd, self.disk.device, self.mountdir ]
426         if self.fstype:
427             args.extend(["-t", self.fstype])
428
429         rc = runner.show(args)
430         if rc != 0:
431             raise MountError("Failed to mount '%s' to '%s' with command '%s'. Retval: %s" %
432                              (self.disk.device, self.mountdir, " ".join(args), rc))
433
434         self.mounted = True
435
436 class ExtDiskMount(DiskMount):
437     """A DiskMount object that is able to format/resize ext[23] filesystems."""
438     def __init__(self, disk, mountdir, fstype, blocksize, fslabel, rmmountdir=True, skipformat = False, fsopts = None):
439         DiskMount.__init__(self, disk, mountdir, fstype, rmmountdir)
440         self.blocksize = blocksize
441         self.fslabel = fslabel.replace("/", "")
442         self.uuid = str(uuid.uuid4())
443         self.skipformat = skipformat
444         self.fsopts = fsopts
445         self.extopts = None
446         self.dumpe2fs = find_binary_path("dumpe2fs")
447         self.tune2fs = find_binary_path("tune2fs")
448
449     def __parse_field(self, output, field):
450         for line in output.split("\n"):
451             if line.startswith(field + ":"):
452                 return line[len(field) + 1:].strip()
453
454         raise KeyError("Failed to find field '%s' in output" % field)
455
456     def __format_filesystem(self):
457         if self.skipformat:
458             msger.debug("Skip filesystem format.")
459             return
460
461         msger.verbose("Formating %s filesystem on %s" % (self.fstype, self.disk.device))
462         cmdlist = [self.mkfscmd, "-F", "-L", self.fslabel, "-m", "1", "-b",
463                    str(self.blocksize), "-U", self.uuid]
464         if self.extopts:
465             cmdlist.extend(self.extopts.split())
466         cmdlist.extend([self.disk.device])
467
468         rc, errout = runner.runtool(cmdlist, catch=2)
469         if rc != 0:
470             raise MountError("Error creating %s filesystem on disk %s:\n%s" %
471                              (self.fstype, self.disk.device, errout))
472
473         if not self.extopts:
474             msger.debug("Tuning filesystem on %s" % self.disk.device)
475             runner.show([self.tune2fs, "-c0", "-i0", "-Odir_index", "-ouser_xattr,acl", self.disk.device])
476
477     def __resize_filesystem(self, size = None):
478         current_size = os.stat(self.disk.lofile)[stat.ST_SIZE]
479
480         if size is None:
481             size = self.disk.size
482
483         if size == current_size:
484             return
485
486         if size > current_size:
487             self.disk.expand(size)
488
489         self.__fsck()
490
491         resize2fs(self.disk.lofile, size)
492         return size
493
494     def __create(self):
495         resize = False
496         if not self.disk.fixed() and self.disk.exists():
497             resize = True
498
499         self.disk.create()
500
501         if resize:
502             self.__resize_filesystem()
503         else:
504             self.__format_filesystem()
505
506     def mount(self, options = None):
507         self.__create()
508         DiskMount.mount(self, options)
509
510     def __fsck(self):
511         msger.info("Checking filesystem %s" % self.disk.lofile)
512         runner.quiet(["/sbin/e2fsck", "-f", "-y", self.disk.lofile])
513
514     def __get_size_from_filesystem(self):
515         return int(self.__parse_field(runner.outs([self.dumpe2fs, '-h', self.disk.lofile]),
516                                       "Block count")) * self.blocksize
517
518     def __resize_to_minimal(self):
519         self.__fsck()
520
521         #
522         # Use a binary search to find the minimal size
523         # we can resize the image to
524         #
525         bot = 0
526         top = self.__get_size_from_filesystem()
527         while top != (bot + 1):
528             t = bot + ((top - bot) / 2)
529
530             if not resize2fs(self.disk.lofile, t):
531                 top = t
532             else:
533                 bot = t
534         return top
535
536     def resparse(self, size = None):
537         self.cleanup()
538         if size == 0:
539             minsize = 0
540         else:
541             minsize = self.__resize_to_minimal()
542             self.disk.truncate(minsize)
543
544         self.__resize_filesystem(size)
545         return minsize
546
547 class VfatDiskMount(DiskMount):
548     """A DiskMount object that is able to format vfat/msdos filesystems."""
549     def __init__(self, disk, mountdir, fstype, blocksize, fslabel, rmmountdir=True, skipformat = False, fsopts = None):
550         DiskMount.__init__(self, disk, mountdir, fstype, rmmountdir)
551         self.blocksize = blocksize
552         self.fslabel = fslabel.replace("/", "")
553         self.uuid = None
554         self.skipformat = skipformat
555         self.fsopts = fsopts
556         self.fsckcmd = find_binary_path("fsck." + self.fstype)
557
558     def __format_filesystem(self):
559         if self.skipformat:
560             msger.debug("Skip filesystem format.")
561             return
562
563         msger.verbose("Formating %s filesystem on %s" % (self.fstype, self.disk.device))
564         rc = runner.show([self.mkfscmd, "-n", self.fslabel, self.disk.device])
565         if rc != 0:
566             raise MountError("Error creating %s filesystem on disk %s" % (self.fstype,self.disk.device))
567
568         msger.verbose("Tuning filesystem on %s" % self.disk.device)
569
570     def __resize_filesystem(self, size = None):
571         current_size = os.stat(self.disk.lofile)[stat.ST_SIZE]
572
573         if size is None:
574             size = self.disk.size
575
576         if size == current_size:
577             return
578
579         if size > current_size:
580             self.disk.expand(size)
581
582         self.__fsck()
583
584         #resize2fs(self.disk.lofile, size)
585         return size
586
587     def __create(self):
588         resize = False
589         if not self.disk.fixed() and self.disk.exists():
590             resize = True
591
592         self.disk.create()
593
594         if resize:
595             self.__resize_filesystem()
596         else:
597             self.__format_filesystem()
598
599     def mount(self, options = None):
600         self.__create()
601         DiskMount.mount(self, options)
602
603     def __fsck(self):
604         msger.debug("Checking filesystem %s" % self.disk.lofile)
605         runner.show([self.fsckcmd, "-y", self.disk.lofile])
606
607     def __get_size_from_filesystem(self):
608         return self.disk.size
609
610     def __resize_to_minimal(self):
611         self.__fsck()
612
613         #
614         # Use a binary search to find the minimal size
615         # we can resize the image to
616         #
617         bot = 0
618         top = self.__get_size_from_filesystem()
619         return top
620
621     def resparse(self, size = None):
622         self.cleanup()
623         minsize = self.__resize_to_minimal()
624         self.disk.truncate(minsize)
625         self.__resize_filesystem(size)
626         return minsize
627
628 class BtrfsDiskMount(DiskMount):
629     """A DiskMount object that is able to format/resize btrfs filesystems."""
630     def __init__(self, disk, mountdir, fstype, blocksize, fslabel, rmmountdir=True, skipformat = False, fsopts = None):
631         self.__check_btrfs()
632         DiskMount.__init__(self, disk, mountdir, fstype, rmmountdir)
633         self.blocksize = blocksize
634         self.fslabel = fslabel.replace("/", "")
635         self.uuid  = None
636         self.skipformat = skipformat
637         self.fsopts = fsopts
638         self.blkidcmd = find_binary_path("blkid")
639         self.btrfsckcmd = find_binary_path("btrfsck")
640
641     def __check_btrfs(self):
642         found = False
643         """ Need to load btrfs module to mount it """
644         load_module("btrfs")
645         for line in open("/proc/filesystems").xreadlines():
646             if line.find("btrfs") > -1:
647                 found = True
648                 break
649         if not found:
650             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.")
651
652         # disable selinux, selinux will block write
653         if os.path.exists("/usr/sbin/setenforce"):
654             runner.show(["/usr/sbin/setenforce", "0"])
655
656     def __parse_field(self, output, field):
657         for line in output.split(" "):
658             if line.startswith(field + "="):
659                 return line[len(field) + 1:].strip().replace("\"", "")
660
661         raise KeyError("Failed to find field '%s' in output" % field)
662
663     def __format_filesystem(self):
664         if self.skipformat:
665             msger.debug("Skip filesystem format.")
666             return
667
668         msger.verbose("Formating %s filesystem on %s" % (self.fstype, self.disk.device))
669         rc = runner.show([self.mkfscmd, "-L", self.fslabel, self.disk.device])
670         if rc != 0:
671             raise MountError("Error creating %s filesystem on disk %s" % (self.fstype,self.disk.device))
672
673         self.uuid = self.__parse_field(runner.outs([self.blkidcmd, self.disk.device]), "UUID")
674
675     def __resize_filesystem(self, size = None):
676         current_size = os.stat(self.disk.lofile)[stat.ST_SIZE]
677
678         if size is None:
679             size = self.disk.size
680
681         if size == current_size:
682             return
683
684         if size > current_size:
685             self.disk.expand(size)
686
687         self.__fsck()
688         return size
689
690     def __create(self):
691         resize = False
692         if not self.disk.fixed() and self.disk.exists():
693             resize = True
694
695         self.disk.create()
696
697         if resize:
698             self.__resize_filesystem()
699         else:
700             self.__format_filesystem()
701
702     def mount(self, options = None):
703         self.__create()
704         DiskMount.mount(self, options)
705
706     def __fsck(self):
707         msger.debug("Checking filesystem %s" % self.disk.lofile)
708         runner.quiet([self.btrfsckcmd, self.disk.lofile])
709
710     def __get_size_from_filesystem(self):
711         return self.disk.size
712
713     def __resize_to_minimal(self):
714         self.__fsck()
715
716         return self.__get_size_from_filesystem()
717
718     def resparse(self, size = None):
719         self.cleanup()
720         minsize = self.__resize_to_minimal()
721         self.disk.truncate(minsize)
722         self.__resize_filesystem(size)
723         return minsize
724
725 class DeviceMapperSnapshot(object):
726     def __init__(self, imgloop, cowloop):
727         self.imgloop = imgloop
728         self.cowloop = cowloop
729
730         self.__created = False
731         self.__name = None
732         self.dmsetupcmd = find_binary_path("dmsetup")
733
734         """Load dm_snapshot if it isn't loaded"""
735         load_module("dm_snapshot")
736
737     def get_path(self):
738         if self.__name is None:
739             return None
740         return os.path.join("/dev/mapper", self.__name)
741     path = property(get_path)
742
743     def create(self):
744         if self.__created:
745             return
746
747         self.imgloop.create()
748         self.cowloop.create()
749
750         self.__name = "imgcreate-%d-%d" % (os.getpid(),
751                                            random.randint(0, 2**16))
752
753         size = os.stat(self.imgloop.lofile)[stat.ST_SIZE]
754
755         table = "0 %d snapshot %s %s p 8" % (size / 512,
756                                              self.imgloop.device,
757                                              self.cowloop.device)
758
759         args = [self.dmsetupcmd, "create", self.__name, "--table", table]
760         if runner.show(args) != 0:
761             self.cowloop.cleanup()
762             self.imgloop.cleanup()
763             raise SnapshotError("Could not create snapshot device using: " + ' '.join(args))
764
765         self.__created = True
766
767     def remove(self, ignore_errors = False):
768         if not self.__created:
769             return
770
771         time.sleep(2)
772         rc = runner.show([self.dmsetupcmd, "remove", self.__name])
773         if not ignore_errors and rc != 0:
774             raise SnapshotError("Could not remove snapshot device")
775
776         self.__name = None
777         self.__created = False
778
779         self.cowloop.cleanup()
780         self.imgloop.cleanup()
781
782     def get_cow_used(self):
783         if not self.__created:
784             return 0
785
786         #
787         # dmsetup status on a snapshot returns e.g.
788         #   "0 8388608 snapshot 416/1048576"
789         # or, more generally:
790         #   "A B snapshot C/D"
791         # where C is the number of 512 byte sectors in use
792         #
793         out = runner.outs([self.dmsetupcmd, "status", self.__name])
794         try:
795             return int((out.split()[3]).split('/')[0]) * 512
796         except ValueError:
797             raise SnapshotError("Failed to parse dmsetup status: " + out)
798
799 def create_image_minimizer(path, image, minimal_size):
800     """
801     Builds a copy-on-write image which can be used to
802     create a device-mapper snapshot of an image where
803     the image's filesystem is as small as possible
804
805     The steps taken are:
806       1) Create a sparse COW
807       2) Loopback mount the image and the COW
808       3) Create a device-mapper snapshot of the image
809          using the COW
810       4) Resize the filesystem to the minimal size
811       5) Determine the amount of space used in the COW
812       6) Restroy the device-mapper snapshot
813       7) Truncate the COW, removing unused space
814       8) Create a squashfs of the COW
815     """
816     imgloop = LoopbackDisk(image, None) # Passing bogus size - doesn't matter
817
818     cowloop = SparseLoopbackDisk(os.path.join(os.path.dirname(path), "osmin"),
819                                  64L * 1024L * 1024L)
820
821     snapshot = DeviceMapperSnapshot(imgloop, cowloop)
822
823     try:
824         snapshot.create()
825
826         resize2fs(snapshot.path, minimal_size)
827
828         cow_used = snapshot.get_cow_used()
829     finally:
830         snapshot.remove(ignore_errors = (not sys.exc_info()[0] is None))
831
832     cowloop.truncate(cow_used)
833
834     mksquashfs(cowloop.lofile, path)
835
836     os.unlink(cowloop.lofile)
837
838 def load_module(module):
839     found = False
840     for line in open('/proc/modules').xreadlines():
841         if line.startswith("%s " % module):
842             found = True
843             break
844     if not found:
845         msger.info("Loading %s..." % module)
846         runner.quiet(['modprobe', module])
847
848 class LoopDevice(object):
849     def __init__(self, loopid=None):
850         self.device = None
851         self.loopid = loopid
852         self.created = False
853         self.kpartxcmd = find_binary_path("kpartx")
854         self.losetupcmd = find_binary_path("losetup")
855
856     def register(self, device):
857         self.device = device
858         self.loopid = None
859         self.created = True
860
861     def reg_atexit(self):
862         import atexit
863         atexit.register(self.close)
864
865     def _genloopid(self):
866         import glob
867         if not glob.glob("/dev/loop[0-9]*"):
868             return 10
869
870         fint = lambda x: x[9:].isdigit() and int(x[9:]) or 0
871         maxid = 1 + max(filter(lambda x: x<100,
872                                map(fint, glob.glob("/dev/loop[0-9]*"))))
873         if maxid < 10: maxid = 10
874         if maxid >= 100: raise
875         return maxid
876
877     def _kpseek(self, device):
878         rc, out = runner.runtool([self.kpartxcmd, '-l', '-v', device])
879         if rc != 0:
880             raise MountError("Can't query dm snapshot on %s" % device)
881         for line in out.splitlines():
882             if line and line.startswith("loop"):
883                 return True
884         return False
885
886     def _loseek(self, device):
887         import re
888         rc, out = runner.runtool([self.losetupcmd, '-a'])
889         if rc != 0:
890             raise MountError("Failed to run 'losetup -a'")
891         for line in out.splitlines():
892             m = re.match("([^:]+): .*", line)
893             if m and m.group(1) == device:
894                 return True
895         return False
896
897     def create(self):
898         if not self.created:
899             if not self.loopid:
900                 self.loopid = self._genloopid()
901             self.device = "/dev/loop%d" % self.loopid
902             if os.path.exists(self.device):
903                 if self._loseek(self.device):
904                     raise MountError("Device busy: %s" % self.device)
905                 else:
906                     self.created = True
907                     return
908
909             mknod = find_binary_path('mknod')
910             rc = runner.show([mknod, '-m664', self.device, 'b', '7', str(self.loopid)])
911             if rc != 0:
912                 raise MountError("Failed to create device %s" % self.device)
913             else:
914                 self.created = True
915
916     def close(self):
917         if self.created:
918             try:
919                 self.cleanup()
920                 self.device = None
921             except MountError, e:
922                 msger.error("%s" % e)
923
924     def cleanup(self):
925
926         if self.device is None:
927             return
928
929
930         if self._kpseek(self.device):
931             if self.created:
932                 for i in range(3, os.sysconf("SC_OPEN_MAX")):
933                     try:
934                         os.close(i)
935                     except:
936                         pass
937             runner.quiet([self.kpartxcmd, "-d", self.device])
938         if self._loseek(self.device):
939             runner.quiet([self.losetupcmd, "-d", self.device])
940         # FIXME: should sleep a while between two loseek
941         if self._loseek(self.device):
942             msger.warning("Can't cleanup loop device %s" % self.device)
943         elif self.loopid:
944             os.unlink(self.device)
945
946 DEVICE_PIDFILE_DIR = "/var/tmp/mic/device"
947 DEVICE_LOCKFILE = "/var/lock/__mic_loopdev.lock"
948
949 def get_loop_device(losetupcmd, lofile):
950     global DEVICE_PIDFILE_DIR
951     global DEVICE_LOCKFILE
952
953     import fcntl
954     makedirs(os.path.dirname(DEVICE_LOCKFILE))
955     fp = open(DEVICE_LOCKFILE, 'w')
956     fcntl.flock(fp, fcntl.LOCK_EX)
957     try:
958         loopdev = None
959         devinst = LoopDevice()
960
961         # clean up left loop device first
962         clean_loop_devices()
963
964         # provide an avaible loop device
965         rc, out = runner.runtool([losetupcmd, "--find"])
966         if rc == 0:
967             loopdev = out.split()[0]
968             devinst.register(loopdev)
969         if not loopdev or not os.path.exists(loopdev):
970             devinst.create()
971             loopdev = devinst.device
972
973         # setup a loop device for image file
974         rc = runner.show([losetupcmd, loopdev, lofile])
975         if rc != 0:
976             raise MountError("Failed to setup loop device for '%s'" % lofile)
977
978         devinst.reg_atexit()
979
980         # try to save device and pid
981         makedirs(DEVICE_PIDFILE_DIR)
982         pidfile = os.path.join(DEVICE_PIDFILE_DIR, os.path.basename(loopdev))
983         if os.path.exists(pidfile):
984             os.unlink(pidfile)
985         with open(pidfile, 'w') as wf:
986             wf.write(str(os.getpid()))
987
988     except MountError, err:
989         raise CreatorError("%s" % str(err))
990     except:
991         raise
992     finally:
993         try:
994             fcntl.flock(fp, fcntl.LOCK_UN)
995             fp.close()
996             os.unlink(DEVICE_LOCKFILE)
997         except:
998             pass
999
1000     return loopdev
1001
1002 def clean_loop_devices(piddir=DEVICE_PIDFILE_DIR):
1003     if not os.path.exists(piddir) or not os.path.isdir(piddir):
1004         return
1005
1006     for loopdev in os.listdir(piddir):
1007         pidfile = os.path.join(piddir, loopdev)
1008         try:
1009             with open(pidfile, 'r') as rf:
1010                 devpid = int(rf.read())
1011         except:
1012             devpid = None
1013
1014         # if the process using this device is alive, skip it
1015         if not devpid or os.path.exists(os.path.join('/proc', str(devpid))):
1016             continue
1017
1018         # try to clean it up
1019         try:
1020             devinst = LoopDevice()
1021             devinst.register(os.path.join('/dev', loopdev))
1022             devinst.cleanup()
1023             os.unlink(pidfile)
1024         except:
1025             pass
1026