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