Ignore the pylint warning of raising-bad-type and unbalanced-tuple-unpacking in conne...
[tools/mic.git] / mic / utils / fs_related.py
1 #!/usr/bin/python3 -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
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 "PATH" in os.environ:
50         paths = os.environ["PATH"].split(":")
51     else:
52         paths = []
53         if "HOME" in os.environ:
54             paths += [os.environ["HOME"] + "/bin"]
55         paths += ["/usr/local/sbin", "/usr/local/bin", "/usr/sbin", "/usr/bin", "/sbin", "/bin"]
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 as 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 as 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, 0o644)
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**31)
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 as 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 F2fsDiskMount(DiskMount):
450     """A DiskMount object that is able to format/resize f2fs filesystems."""
451     def __init__(self, disk, mountdir, fstype, blocksize, fslabel, rmmountdir=True, skipformat = False, fsopts = None, fsuuid=None):
452         self.__check_f2fs()
453         DiskMount.__init__(self, disk, mountdir, fstype, rmmountdir)
454         self.blocksize = blocksize
455         self.fslabel = fslabel.replace("/", "")
456         self.uuid = fsuuid or None
457         self.skipformat = skipformat
458         self.fsopts = fsopts
459         self.__f2fsopts = None
460         self.blkidcmd = find_binary_path("blkid")
461         self.dumpe2fs = find_binary_path("dump." + self.fstype)
462         self.fsckcmd = find_binary_path("fsck." + self.fstype)
463         self.resizecmd = find_binary_path("resize." + self.fstype)
464
465     def __get_f2fsopts(self):
466         return self.__f2f2opts
467
468     def __set_f2fsopts(self, val):
469         if val is None:
470             self.__f2fsopts = None
471         else:
472             self.__f2fsopts = val
473     f2fsopts = property(__get_f2fsopts, __set_f2fsopts)
474
475     def __check_f2fs(self):
476         found = False
477         """ Need to load f2fs module to mount it """
478         load_module("f2fs")
479         for line in open("/proc/filesystems"):
480             if line.find("f2fs") > -1:
481                 found = True
482                 break
483         if not found:
484             raise MountError("Your system can't mount f2fs filesystem, please make sure your kernel has f2fs support and the module f2fs.ko has been loaded.")
485
486     def __parse_field(self, output, field):
487         for line in output.split(" "):
488             if line.startswith(field + "="):
489                 return line[len(field) + 1:].strip().replace("\"", "")
490
491         raise KeyError("Failed to find field '%s' in output" % field)
492
493     def __format_filesystem(self):
494         if self.skipformat:
495             msger.debug("Skip filesystem format.")
496             return
497
498         msger.verbose("Formating %s filesystem on %s" % (self.fstype, self.disk.device))
499
500         cmdlist = [self.mkfscmd, "-l", self.fslabel]
501         if self.__f2fsopts:
502             cmdlist.extend(self._f2fsopts.split())
503         cmdlist.extend([self.disk.device])
504
505         rc, errout = runner.runtool(cmdlist, catch=2)
506         if rc != 0:
507             raise MountError("Error creating %s filesystem on disk %s:\n%s" %
508                              (self.fstype, self.disk.device, errout))
509
510         self.uuid = self.__parse_field(runner.outs([self.blkidcmd, self.disk.device]), "UUID")
511
512     def __resize_filesystem(self, size = None):
513         msger.info("Resizing filesystem ...")
514         current_size = os.stat(self.disk.lofile)[stat.ST_SIZE]
515
516         if size is None:
517             size = self.disk.size
518
519         if size == current_size:
520             return
521
522         if size > current_size:
523             self.disk.expand(size=size)
524
525         self.__fsck()
526
527         return size
528
529     def __create(self):
530         resize = False
531         if not self.disk.fixed() and self.disk.exists():
532             resize = True
533
534         self.disk.create()
535
536         if resize:
537             self.__resize_filesystem()
538         else:
539             self.__format_filesystem()
540
541     def mount(self, options = None, init_expand = False):
542         self.__create()
543         if init_expand:
544             expand_size = int(self.disk.size * 1.5)
545             msger.info("Initial partition size expanded : %ld -> %ld" % (self.disk.size, expand_size))
546             self.__resize_filesystem(expand_size)
547             self.disk.reread_size()
548         DiskMount.mount(self, options)
549
550     def __fsck(self):
551         msger.info("Checking filesystem %s" % self.disk.lofile)
552         runner.quiet([self.fsckcmd, self.disk.lofile])
553
554     def __get_size_from_filesystem(self):
555         return self.disk.size
556
557     def __resize_to_minimal(self):
558         msger.info("Resizing filesystem to minimal ...")
559         self.__fsck()
560
561         return self.__get_size_from_filesystem()
562
563     def resparse(self, size = None):
564         self.cleanup()
565         if size == 0:
566             minsize = 0
567         else:
568             minsize = self.__resize_to_minimal()
569             self.disk.truncate(minsize)
570             self.__resize_filesystem(size)
571         return minsize
572
573 class ExtDiskMount(DiskMount):
574     """A DiskMount object that is able to format/resize ext[23] filesystems."""
575     def __init__(self, disk, mountdir, fstype, blocksize, fslabel, rmmountdir=True, skipformat = False, fsopts = None, fsuuid=None):
576         DiskMount.__init__(self, disk, mountdir, fstype, rmmountdir)
577         self.blocksize = blocksize
578         self.fslabel = fslabel.replace("/", "")
579         self.uuid = fsuuid or str(uuid.uuid4())
580         self.skipformat = skipformat
581         self.fsopts = fsopts
582         self.__extopts = None
583         self.dumpe2fs = find_binary_path("dumpe2fs")
584         self.tune2fs = find_binary_path("tune2fs")
585
586     def __get_extopts(self):
587         return self.__extopts
588
589     def __set_extopts(self, val):
590         if val is None:
591             self.__extopts = None
592         else:
593             m = re.search(r'-b\s*(?P<blocksize>\d+)', val)
594             if m:
595                 self.blocksize = int(m.group('blocksize'))
596                 val = val.replace(m.group(), '')
597             self.__extopts = val
598
599     extopts = property(__get_extopts, __set_extopts)
600
601     def __parse_field(self, output, field):
602         for line in output.split("\n"):
603             if line.startswith(field + ":"):
604                 return line[len(field) + 1:].strip()
605
606         raise KeyError("Failed to find field '%s' in output" % field)
607
608     def __format_filesystem(self):
609         if self.skipformat:
610             msger.debug("Skip filesystem format.")
611             return
612
613         msger.verbose("Formating %s filesystem on %s" % (self.fstype, self.disk.device))
614         cmdlist = [self.mkfscmd, "-F", "-L", self.fslabel, "-m", "1", "-b",
615                    str(self.blocksize), "-U", self.uuid]
616         if self.__extopts:
617             cmdlist.extend(self.__extopts.split())
618         cmdlist.extend([self.disk.device])
619
620         rc, errout = runner.runtool(cmdlist, catch=2)
621         if rc != 0:
622             raise MountError("Error creating %s filesystem on disk %s:\n%s" %
623                              (self.fstype, self.disk.device, errout))
624
625         if not self.__extopts:
626             msger.debug("Tuning filesystem on %s" % self.disk.device)
627             runner.show([self.tune2fs, "-c0", "-i0", "-Odir_index", "-ouser_xattr,acl", self.disk.device])
628
629     def __resize_filesystem(self, size = None):
630         msger.info("Resizing filesystem ...")
631         current_size = os.stat(self.disk.lofile)[stat.ST_SIZE]
632
633         if size is None:
634             size = self.disk.size
635
636         if size == current_size:
637             return
638
639         if size > current_size:
640             self.disk.expand(size=size)
641
642         self.__fsck()
643
644         resize2fs(self.disk.lofile, size)
645         if size and size != os.stat(self.disk.lofile)[stat.ST_SIZE]:
646             raise MountError("Failed to resize filesystem %s to %d " % (self.disk.lofile, size))
647
648         return size
649
650     def __create(self):
651         resize = False
652         if not self.disk.fixed() and self.disk.exists():
653             resize = True
654
655         self.disk.create()
656
657         if resize:
658             self.__resize_filesystem()
659         else:
660             self.__format_filesystem()
661
662     def mount(self, options = None, init_expand = False):
663         self.__create()
664         if init_expand:
665             expand_size = int(self.disk.size * 1.5)
666             msger.info("Initial partition size expanded : %ld -> %ld" % (self.disk.size, expand_size))
667             self.__resize_filesystem(expand_size)
668             self.disk.reread_size()
669         DiskMount.mount(self, options)
670
671     def __fsck(self):
672         msger.info("Checking filesystem %s" % self.disk.lofile)
673         runner.quiet(["/sbin/e2fsck", "-f", "-y", self.disk.lofile])
674
675     def __get_size_from_filesystem(self):
676         return int(self.__parse_field(runner.outs([self.dumpe2fs, '-h', self.disk.lofile]),
677                                       "Block count")) * self.blocksize
678
679     def __resize_to_minimal(self):
680         msger.info("Resizing filesystem to minimal ...")
681         self.__fsck()
682         resize2fs(self.disk.lofile, 0)
683         return self.__get_size_from_filesystem()
684
685     def resparse(self, size = None):
686         self.cleanup()
687         if size == 0:
688             minsize = 0
689         else:
690             minsize = self.__resize_to_minimal()
691             self.disk.truncate(minsize)
692
693         self.__resize_filesystem(size)
694         return minsize
695
696 class VfatDiskMount(DiskMount):
697     """A DiskMount object that is able to format vfat/msdos filesystems."""
698     def __init__(self, disk, mountdir, fstype, blocksize, fslabel, rmmountdir=True, skipformat = False, fsopts = None, fsuuid = None):
699         DiskMount.__init__(self, disk, mountdir, fstype, rmmountdir)
700         self.blocksize = blocksize
701         self.fslabel = fslabel.replace("/", "")
702         rand1 = random.randint(0, 2**16 - 1)
703         rand2 = random.randint(0, 2**16 - 1)
704         self.uuid = fsuuid or "%04X-%04X" % (rand1, rand2)
705         self.skipformat = skipformat
706         self.fsopts = fsopts
707         self.fsckcmd = find_binary_path("fsck." + self.fstype)
708
709     def __format_filesystem(self):
710         if self.skipformat:
711             msger.debug("Skip filesystem format.")
712             return
713
714         msger.verbose("Formating %s filesystem on %s" % (self.fstype, self.disk.device))
715         rc = runner.show([self.mkfscmd, "-n", self.fslabel,
716                           "-i", self.uuid.replace("-", ""), self.disk.device])
717         if rc != 0:
718             raise MountError("Error creating %s filesystem on disk %s" % (self.fstype,self.disk.device))
719
720         msger.verbose("Tuning filesystem on %s" % self.disk.device)
721
722     def __resize_filesystem(self, size = None):
723         current_size = os.stat(self.disk.lofile)[stat.ST_SIZE]
724
725         if size is None:
726             size = self.disk.size
727
728         if size == current_size:
729             return
730
731         if size > current_size:
732             self.disk.expand(size=size)
733
734         self.__fsck()
735
736         #resize2fs(self.disk.lofile, size)
737         return size
738
739     def __create(self):
740         resize = False
741         if not self.disk.fixed() and self.disk.exists():
742             resize = True
743
744         self.disk.create()
745
746         if resize:
747             self.__resize_filesystem()
748         else:
749             self.__format_filesystem()
750
751     def mount(self, options = None, init_expand = False):
752         self.__create()
753         if init_expand:
754             expand_size = int(self.disk.size * 1.5)
755             msger.info("Initial partition size expanded : %ld -> %ld" % (self.disk.size, expand_size))
756             self.__resize_filesystem(expand_size)
757             self.disk.reread_size()
758         DiskMount.mount(self, options)
759
760     def __fsck(self):
761         msger.debug("Checking filesystem %s" % self.disk.lofile)
762         runner.show([self.fsckcmd, "-y", self.disk.lofile])
763
764     def __get_size_from_filesystem(self):
765         return self.disk.size
766
767     def __resize_to_minimal(self):
768         self.__fsck()
769
770         #
771         # Use a binary search to find the minimal size
772         # we can resize the image to
773         #
774         bot = 0
775         top = self.__get_size_from_filesystem()
776         return top
777
778     def resparse(self, size = None):
779         self.cleanup()
780         minsize = self.__resize_to_minimal()
781         self.disk.truncate(minsize)
782         self.__resize_filesystem(size)
783         return minsize
784
785 class BtrfsDiskMount(DiskMount):
786     """A DiskMount object that is able to format/resize btrfs filesystems."""
787     def __init__(self, disk, mountdir, fstype, blocksize, fslabel, rmmountdir=True, skipformat = False, fsopts = None, fsuuid = None):
788         self.__check_btrfs()
789         DiskMount.__init__(self, disk, mountdir, fstype, rmmountdir)
790         self.blocksize = blocksize
791         self.fslabel = fslabel.replace("/", "")
792         self.uuid  = fsuuid or None
793         self.skipformat = skipformat
794         self.fsopts = fsopts
795         self.blkidcmd = find_binary_path("blkid")
796         self.btrfsckcmd = find_binary_path("btrfsck")
797
798     def __check_btrfs(self):
799         found = False
800         """ Need to load btrfs module to mount it """
801         load_module("btrfs")
802         for line in open("/proc/filesystems"):
803             if line.find("btrfs") > -1:
804                 found = True
805                 break
806         if not found:
807             raise MountError("Your system can't mount btrfs filesystem, please make sure your kernel has btrfs support and the module btrfs.ko has been loaded.")
808
809         # disable selinux, selinux will block write
810         if os.path.exists("/usr/sbin/setenforce"):
811             runner.show(["/usr/sbin/setenforce", "0"])
812
813     def __parse_field(self, output, field):
814         for line in output.split(" "):
815             if line.startswith(field + "="):
816                 return line[len(field) + 1:].strip().replace("\"", "")
817
818         raise KeyError("Failed to find field '%s' in output" % field)
819
820     def __format_filesystem(self):
821         if self.skipformat:
822             msger.debug("Skip filesystem format.")
823             return
824
825         msger.verbose("Formating %s filesystem on %s" % (self.fstype, self.disk.device))
826         rc = runner.show([self.mkfscmd, "-L", self.fslabel, "-m", "single", self.disk.device])
827         if rc != 0:
828             raise MountError("Error creating %s filesystem on disk %s" % (self.fstype,self.disk.device))
829
830         self.uuid = self.__parse_field(runner.outs([self.blkidcmd, self.disk.device]), "UUID")
831
832     def __resize_filesystem(self, size = None):
833         current_size = os.stat(self.disk.lofile)[stat.ST_SIZE]
834
835         if size is None:
836             size = self.disk.size
837
838         if size == current_size:
839             return
840
841         if size > current_size:
842             self.disk.expand(size=size)
843
844         self.__fsck()
845         return size
846
847     def __create(self):
848         resize = False
849         if not self.disk.fixed() and self.disk.exists():
850             resize = True
851
852         self.disk.create()
853
854         if resize:
855             self.__resize_filesystem()
856         else:
857             self.__format_filesystem()
858
859     def mount(self, options = None, init_expand = False):
860         self.__create()
861         if init_expand:
862             expand_size = int(self.disk.size * 1.5)
863             msger.info("Initial partition size expanded : %ld -> %ld" % (self.disk.size, expand_size))
864             self.__resize_filesystem(expand_size)
865             self.disk.reread_size()
866         DiskMount.mount(self, options)
867
868     def __fsck(self):
869         msger.debug("Checking filesystem %s" % self.disk.lofile)
870         runner.quiet([self.btrfsckcmd, self.disk.lofile])
871
872     def __get_size_from_filesystem(self):
873         return self.disk.size
874
875     def __resize_to_minimal(self):
876         self.__fsck()
877
878         return self.__get_size_from_filesystem()
879
880     def resparse(self, size = None):
881         self.cleanup()
882         minsize = self.__resize_to_minimal()
883         self.disk.truncate(minsize)
884         self.__resize_filesystem(size)
885         return minsize
886
887 class DeviceMapperSnapshot(object):
888     def __init__(self, imgloop, cowloop):
889         self.imgloop = imgloop
890         self.cowloop = cowloop
891
892         self.__created = False
893         self.__name = None
894         self.dmsetupcmd = find_binary_path("dmsetup")
895
896         """Load dm_snapshot if it isn't loaded"""
897         load_module("dm_snapshot")
898
899     def get_path(self):
900         if self.__name is None:
901             return None
902         return os.path.join("/dev/mapper", self.__name)
903     path = property(get_path)
904
905     def create(self):
906         if self.__created:
907             return
908
909         self.imgloop.create()
910         self.cowloop.create()
911
912         self.__name = "imgcreate-%d-%d" % (os.getpid(),
913                                            random.randint(0, 2**16))
914
915         size = os.stat(self.imgloop.lofile)[stat.ST_SIZE]
916
917         table = "0 %d snapshot %s %s p 8" % (size / 512,
918                                              self.imgloop.device,
919                                              self.cowloop.device)
920
921         args = [self.dmsetupcmd, "create", self.__name, "--table", table]
922         if runner.show(args) != 0:
923             self.cowloop.cleanup()
924             self.imgloop.cleanup()
925             raise SnapshotError("Could not create snapshot device using: " + ' '.join(args))
926
927         self.__created = True
928
929     def remove(self, ignore_errors = False):
930         if not self.__created:
931             return
932
933         time.sleep(2)
934         rc = runner.show([self.dmsetupcmd, "remove", self.__name])
935         if not ignore_errors and rc != 0:
936             raise SnapshotError("Could not remove snapshot device")
937
938         self.__name = None
939         self.__created = False
940
941         self.cowloop.cleanup()
942         self.imgloop.cleanup()
943
944     def get_cow_used(self):
945         if not self.__created:
946             return 0
947
948         #
949         # dmsetup status on a snapshot returns e.g.
950         #   "0 8388608 snapshot 416/1048576"
951         # or, more generally:
952         #   "A B snapshot C/D"
953         # where C is the number of 512 byte sectors in use
954         #
955         out = runner.outs([self.dmsetupcmd, "status", self.__name])
956         try:
957             return int((out.split()[3]).split('/')[0]) * 512
958         except ValueError:
959             raise SnapshotError("Failed to parse dmsetup status: " + out)
960
961 def create_image_minimizer(path, image, minimal_size):
962     """
963     Builds a copy-on-write image which can be used to
964     create a device-mapper snapshot of an image where
965     the image's filesystem is as small as possible
966
967     The steps taken are:
968       1) Create a sparse COW
969       2) Loopback mount the image and the COW
970       3) Create a device-mapper snapshot of the image
971          using the COW
972       4) Resize the filesystem to the minimal size
973       5) Determine the amount of space used in the COW
974       6) Restroy the device-mapper snapshot
975       7) Truncate the COW, removing unused space
976       8) Create a squashfs of the COW
977     """
978     imgloop = LoopbackDisk(image, None) # Passing bogus size - doesn't matter
979
980     cowloop = SparseLoopbackDisk(os.path.join(os.path.dirname(path), "osmin"),
981                                  64 * 1024 * 1024)
982
983     snapshot = DeviceMapperSnapshot(imgloop, cowloop)
984
985     try:
986         snapshot.create()
987
988         resize2fs(snapshot.path, minimal_size)
989
990         cow_used = snapshot.get_cow_used()
991     finally:
992         snapshot.remove(ignore_errors = (not sys.exc_info()[0] is None))
993
994     cowloop.truncate(cow_used)
995
996     mksquashfs(cowloop.lofile, path)
997
998     os.unlink(cowloop.lofile)
999
1000 def load_module(module):
1001     found = False
1002     for line in open('/proc/modules'):
1003         if line.startswith("%s " % module):
1004             found = True
1005             break
1006     if not found:
1007         msger.info("Loading %s..." % module)
1008         runner.quiet(['modprobe', module])
1009
1010 class LoopDevice(object):
1011     def __init__(self, loopid=None):
1012         self.device = None
1013         self.loopid = loopid
1014         self.created = False
1015         self.kpartxcmd = find_binary_path("kpartx")
1016         self.losetupcmd = find_binary_path("losetup")
1017
1018     def register(self, device):
1019         self.device = device
1020         self.loopid = None
1021         #self.created = True
1022
1023     def reg_atexit(self):
1024         import atexit
1025         atexit.register(self.close)
1026
1027     def _genloopid(self):
1028         import glob
1029         if not glob.glob("/dev/loop[0-9]*"):
1030             return 10
1031
1032         fint = lambda x: x[9:].isdigit() and int(x[9:]) or 0
1033         maxid = 1 + max([x for x in map(fint, glob.glob("/dev/loop[0-9]*")) if x<256])
1034         if maxid < 10: maxid = 10
1035         if maxid >= 256:
1036             raise Exception("maxid >= 256")
1037         return maxid
1038
1039     def _kpseek(self, device):
1040         rc, out = runner.runtool([self.kpartxcmd, '-l', '-v', device])
1041         if rc != 0:
1042             raise MountError("Can't query dm snapshot on %s" % device)
1043         for line in out.splitlines():
1044             if line and line.startswith("loop"):
1045                 return True
1046         return False
1047
1048     def _loseek(self, device):
1049         import re
1050         rc, out = runner.runtool([self.losetupcmd, '-a'])
1051         if rc != 0:
1052             raise MountError("Failed to run 'losetup -a'")
1053         for line in out.splitlines():
1054             m = re.match("([^:]+): .*", line)
1055             if m and m.group(1) == device:
1056                 return True
1057         return False
1058
1059     def create(self):
1060         if not self.created:
1061             if not self.loopid:
1062                 self.loopid = self._genloopid()
1063             self.device = "/dev/loop%d" % self.loopid
1064             if os.path.exists(self.device):
1065                 if self._loseek(self.device):
1066                     raise MountError("Device busy: %s" % self.device)
1067                 else:
1068                     self.created = True
1069                     return
1070
1071             mknod = find_binary_path('mknod')
1072             rc = runner.show([mknod, '-m664', self.device, 'b', '7', str(self.loopid)])
1073             if rc != 0:
1074                 raise MountError("Failed to create device %s" % self.device)
1075             else:
1076                 self.created = True
1077
1078     def close(self):
1079         if self.created:
1080             try:
1081                 self.cleanup()
1082                 self.device = None
1083             except MountError as e:
1084                 raise CreatorError("%s" % e)
1085
1086     def cleanup(self):
1087
1088         if self.device is None:
1089             return
1090
1091
1092         if self._kpseek(self.device):
1093             runner.quiet([self.kpartxcmd, "-d", self.device])
1094         if self._loseek(self.device):
1095             runner.quiet([self.losetupcmd, "-d", self.device])
1096         # FIXME: should sleep a while between two loseek
1097         if self._loseek(self.device):
1098             msger.warning("Can't cleanup loop device %s" % self.device)
1099         elif self.loopid:
1100             os.unlink(self.device)
1101
1102 DEVICE_PIDFILE_DIR = "/var/tmp/mic/device"
1103 DEVICE_LOCKFILE = "/var/lock/__mic_loopdev.lock"
1104
1105 def get_loop_device(losetupcmd, lofile):
1106     import fcntl
1107     makedirs(os.path.dirname(DEVICE_LOCKFILE))
1108     fp = open(DEVICE_LOCKFILE, 'w')
1109     fcntl.flock(fp, fcntl.LOCK_EX)
1110     try:
1111         loopdev = None
1112         devinst = LoopDevice()
1113
1114         # clean up left loop device first
1115         clean_loop_devices()
1116
1117         # provide an avaible loop device
1118         rc, out = runner.runtool([losetupcmd, "-f"])
1119         if rc == 0 and out:
1120             loopdev = out.split()[0]
1121             devinst.register(loopdev)
1122         if not loopdev or not os.path.exists(loopdev):
1123             devinst.create()
1124             loopdev = devinst.device
1125
1126         # setup a loop device for image file
1127         rc = runner.show([losetupcmd, loopdev, lofile])
1128         if rc != 0:
1129             raise MountError("Failed to setup loop device for '%s'" % lofile)
1130
1131         devinst.reg_atexit()
1132
1133         # try to save device and pid
1134         makedirs(DEVICE_PIDFILE_DIR)
1135         pidfile = os.path.join(DEVICE_PIDFILE_DIR, os.path.basename(loopdev))
1136         if os.path.exists(pidfile):
1137             os.unlink(pidfile)
1138         with open(pidfile, 'w') as wf:
1139             wf.write(str(os.getpid()))
1140
1141     except MountError as err:
1142         raise CreatorError("%s" % str(err))
1143     except:
1144         raise
1145     finally:
1146         try:
1147             fcntl.flock(fp, fcntl.LOCK_UN)
1148             fp.close()
1149         except:
1150             pass
1151
1152     return loopdev
1153
1154 def clean_loop_devices(piddir=DEVICE_PIDFILE_DIR):
1155     if not os.path.exists(piddir) or not os.path.isdir(piddir):
1156         return
1157
1158     for loopdev in os.listdir(piddir):
1159         pidfile = os.path.join(piddir, loopdev)
1160         try:
1161             with open(pidfile, 'r') as rf:
1162                 devpid = int(rf.read())
1163         except:
1164             devpid = None
1165
1166         # if the process using this device is alive, skip it
1167         if not devpid or os.path.exists(os.path.join('/proc', str(devpid))):
1168             continue
1169
1170         # try to clean it up
1171         try:
1172             devinst = LoopDevice()
1173             devinst.register(os.path.join('/dev', loopdev))
1174             devinst.cleanup()
1175             os.unlink(pidfile)
1176         except:
1177             pass
1178