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