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