9be1a101a54d96b035dda2a6652b47bb93a62054
[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 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     def reread_size(self):
308         if self.device is None:
309             return
310         msger.debug("Reread size %s" % self.device)
311         rc = runner.show([self.losetupcmd, "-c", self.device])
312         # XXX: (WORKAROUND) re-setup loop device when losetup isn't support '-c' option.
313         if rc != 0:
314             msger.debug("Fail to reread size, Try to re-setup loop device to reread the size of the file associated")
315             runner.show([self.losetupcmd, "-d", self.device])
316             runner.show([self.losetupcmd, self.device, self.lofile])
317
318 class SparseLoopbackDisk(LoopbackDisk):
319     """A Disk backed by a sparse file via the loop module."""
320     def __init__(self, lofile, size):
321         LoopbackDisk.__init__(self, lofile, size)
322
323     def expand(self, create = False, size = None):
324         flags = os.O_WRONLY
325         if create:
326             flags |= os.O_CREAT
327             if not os.path.exists(self.lofile):
328                 makedirs(os.path.dirname(self.lofile))
329
330         if size is None:
331             size = self.size
332
333         msger.debug("Extending sparse file %s to %d" % (self.lofile, size))
334         if create:
335             fd = os.open(self.lofile, flags, 0644)
336         else:
337             fd = os.open(self.lofile, flags)
338
339         if size <= 0:
340             size = 1
341         try:
342             os.ftruncate(fd, size)
343         except:
344             # may be limited by 2G in 32bit env
345             os.ftruncate(fd, 2**31L)
346
347         os.close(fd)
348
349     def truncate(self, size = None):
350         if size is None:
351             size = self.size
352
353         msger.debug("Truncating sparse file %s to %d" % (self.lofile, size))
354         fd = os.open(self.lofile, os.O_WRONLY)
355         os.ftruncate(fd, size)
356         os.close(fd)
357
358     def create(self):
359         if self.device is not None:
360             return
361
362         self.expand(create = True)
363         LoopbackDisk.create(self)
364
365 class Mount:
366     """A generic base class to deal with mounting things."""
367     def __init__(self, mountdir):
368         self.mountdir = mountdir
369
370     def cleanup(self):
371         self.unmount()
372
373     def mount(self, options = None):
374         pass
375
376     def unmount(self):
377         pass
378
379 class DiskMount(Mount):
380     """A Mount object that handles mounting of a Disk."""
381     def __init__(self, disk, mountdir, fstype = None, rmmountdir = True):
382         Mount.__init__(self, mountdir)
383
384         self.disk = disk
385         self.fstype = fstype
386         self.rmmountdir = rmmountdir
387
388         self.mounted = False
389         self.rmdir   = False
390         if fstype:
391             self.mkfscmd = find_binary_path("mkfs." + self.fstype)
392         else:
393             self.mkfscmd = None
394         self.mountcmd = find_binary_path("mount")
395         self.umountcmd = find_binary_path("umount")
396
397     def cleanup(self):
398         Mount.cleanup(self)
399         self.disk.cleanup()
400
401     def unmount(self):
402         if self.mounted:
403             msger.debug("Unmounting directory %s" % self.mountdir)
404             runner.quiet('sync') # sync the data on this mount point
405             rc = runner.show([self.umountcmd, "-l", self.mountdir])
406             if rc == 0:
407                 self.mounted = False
408             else:
409                 raise MountError("Failed to umount %s" % self.mountdir)
410         if self.rmdir and not self.mounted:
411             try:
412                 os.rmdir(self.mountdir)
413             except OSError, e:
414                 pass
415             self.rmdir = False
416
417
418     def __create(self):
419         self.disk.create()
420
421
422     def mount(self, options = None):
423         if self.mounted:
424             return
425
426         if not os.path.isdir(self.mountdir):
427             msger.debug("Creating mount point %s" % self.mountdir)
428             os.makedirs(self.mountdir)
429             self.rmdir = self.rmmountdir
430
431         self.__create()
432
433         msger.debug("Mounting %s at %s" % (self.disk.device, self.mountdir))
434         if options:
435             args = [ self.mountcmd, "-o", options, self.disk.device, self.mountdir ]
436         else:
437             args = [ self.mountcmd, self.disk.device, self.mountdir ]
438         if self.fstype:
439             args.extend(["-t", self.fstype])
440
441         rc = runner.show(args)
442         if rc != 0:
443             raise MountError("Failed to mount '%s' to '%s' with command '%s'. Retval: %s" %
444                              (self.disk.device, self.mountdir, " ".join(args), rc))
445
446         self.mounted = True
447
448 class ExtDiskMount(DiskMount):
449     """A DiskMount object that is able to format/resize ext[23] filesystems."""
450     def __init__(self, disk, mountdir, fstype, blocksize, fslabel, rmmountdir=True, skipformat = False, fsopts = None, fsuuid=None):
451         DiskMount.__init__(self, disk, mountdir, fstype, rmmountdir)
452         self.blocksize = blocksize
453         self.fslabel = fslabel.replace("/", "")
454         self.uuid = fsuuid or str(uuid.uuid4())
455         self.skipformat = skipformat
456         self.fsopts = fsopts
457         self.extopts = None
458         self.dumpe2fs = find_binary_path("dumpe2fs")
459         self.tune2fs = find_binary_path("tune2fs")
460
461     def __parse_field(self, output, field):
462         for line in output.split("\n"):
463             if line.startswith(field + ":"):
464                 return line[len(field) + 1:].strip()
465
466         raise KeyError("Failed to find field '%s' in output" % field)
467
468     def __format_filesystem(self):
469         if self.skipformat:
470             msger.debug("Skip filesystem format.")
471             return
472
473         msger.verbose("Formating %s filesystem on %s" % (self.fstype, self.disk.device))
474         cmdlist = [self.mkfscmd, "-F", "-L", self.fslabel, "-m", "1", "-b",
475                    str(self.blocksize), "-U", self.uuid]
476         if self.extopts:
477             cmdlist.extend(self.extopts.split())
478         cmdlist.extend([self.disk.device])
479
480         rc, errout = runner.runtool(cmdlist, catch=2)
481         if rc != 0:
482             raise MountError("Error creating %s filesystem on disk %s:\n%s" %
483                              (self.fstype, self.disk.device, errout))
484
485         if not self.extopts:
486             msger.debug("Tuning filesystem on %s" % self.disk.device)
487             runner.show([self.tune2fs, "-c0", "-i0", "-Odir_index", "-ouser_xattr,acl", self.disk.device])
488
489     def __resize_filesystem(self, size = None):
490         msger.info("Resizing filesystem ...")
491         current_size = os.stat(self.disk.lofile)[stat.ST_SIZE]
492
493         if size is None:
494             size = self.disk.size
495
496         if size == current_size:
497             return
498
499         if size > current_size:
500             self.disk.expand(size=size)
501
502         self.__fsck()
503
504         resize2fs(self.disk.lofile, size)
505         if size and size != os.stat(self.disk.lofile)[stat.ST_SIZE]:
506             raise MountError("Failed to resize filesystem %s to " % (self.disk.lofile, size))
507
508         return size
509
510     def __create(self):
511         resize = False
512         if not self.disk.fixed() and self.disk.exists():
513             resize = True
514
515         self.disk.create()
516
517         if resize:
518             self.__resize_filesystem()
519         else:
520             self.__format_filesystem()
521
522     def mount(self, options = None, init_expand = False):
523         self.__create()
524         if init_expand:
525             expand_size = long(self.disk.size * 1.5)
526             msger.info("Initial partition size expanded : %ld -> %ld" % (self.disk.size, expand_size))
527             self.__resize_filesystem(expand_size)
528             self.disk.reread_size()
529         DiskMount.mount(self, options)
530
531     def __fsck(self):
532         msger.info("Checking filesystem %s" % self.disk.lofile)
533         runner.quiet(["/sbin/e2fsck", "-f", "-y", self.disk.lofile])
534
535     def __get_size_from_filesystem(self):
536         return int(self.__parse_field(runner.outs([self.dumpe2fs, '-h', self.disk.lofile]),
537                                       "Block count")) * self.blocksize
538
539     def __resize_to_minimal(self):
540         msger.info("Resizing filesystem to minimal ...")
541         self.__fsck()
542         resize2fs(self.disk.lofile, 0)
543         return self.__get_size_from_filesystem()
544
545     def resparse(self, size = None):
546         self.cleanup()
547         if size == 0:
548             minsize = 0
549         else:
550             minsize = self.__resize_to_minimal()
551             self.disk.truncate(minsize)
552
553         self.__resize_filesystem(size)
554         return minsize
555
556 class VfatDiskMount(DiskMount):
557     """A DiskMount object that is able to format vfat/msdos filesystems."""
558     def __init__(self, disk, mountdir, fstype, blocksize, fslabel, rmmountdir=True, skipformat = False, fsopts = None, fsuuid = None):
559         DiskMount.__init__(self, disk, mountdir, fstype, rmmountdir)
560         self.blocksize = blocksize
561         self.fslabel = fslabel.replace("/", "")
562         rand1 = random.randint(0, 2**16 - 1)
563         rand2 = random.randint(0, 2**16 - 1)
564         self.uuid = fsuuid or "%04X-%04X" % (rand1, rand2)
565         self.skipformat = skipformat
566         self.fsopts = fsopts
567         self.fsckcmd = find_binary_path("fsck." + self.fstype)
568
569     def __format_filesystem(self):
570         if self.skipformat:
571             msger.debug("Skip filesystem format.")
572             return
573
574         msger.verbose("Formating %s filesystem on %s" % (self.fstype, self.disk.device))
575         rc = runner.show([self.mkfscmd, "-n", self.fslabel,
576                           "-i", self.uuid.replace("-", ""), self.disk.device])
577         if rc != 0:
578             raise MountError("Error creating %s filesystem on disk %s" % (self.fstype,self.disk.device))
579
580         msger.verbose("Tuning filesystem on %s" % self.disk.device)
581
582     def __resize_filesystem(self, size = None):
583         current_size = os.stat(self.disk.lofile)[stat.ST_SIZE]
584
585         if size is None:
586             size = self.disk.size
587
588         if size == current_size:
589             return
590
591         if size > current_size:
592             self.disk.expand(size=size)
593
594         self.__fsck()
595
596         #resize2fs(self.disk.lofile, size)
597         return size
598
599     def __create(self):
600         resize = False
601         if not self.disk.fixed() and self.disk.exists():
602             resize = True
603
604         self.disk.create()
605
606         if resize:
607             self.__resize_filesystem()
608         else:
609             self.__format_filesystem()
610
611     def mount(self, options = None, init_expand = False):
612         self.__create()
613         if init_expand:
614             expand_size = long(self.disk.size * 1.5)
615             msger.info("Initial partition size expanded : %ld -> %ld" % (self.disk.size, expand_size))
616             self.__resize_filesystem(expand_size)
617             self.disk.reread_size()
618         DiskMount.mount(self, options)
619
620     def __fsck(self):
621         msger.debug("Checking filesystem %s" % self.disk.lofile)
622         runner.show([self.fsckcmd, "-y", self.disk.lofile])
623
624     def __get_size_from_filesystem(self):
625         return self.disk.size
626
627     def __resize_to_minimal(self):
628         self.__fsck()
629
630         #
631         # Use a binary search to find the minimal size
632         # we can resize the image to
633         #
634         bot = 0
635         top = self.__get_size_from_filesystem()
636         return top
637
638     def resparse(self, size = None):
639         self.cleanup()
640         minsize = self.__resize_to_minimal()
641         self.disk.truncate(minsize)
642         self.__resize_filesystem(size)
643         return minsize
644
645 class BtrfsDiskMount(DiskMount):
646     """A DiskMount object that is able to format/resize btrfs filesystems."""
647     def __init__(self, disk, mountdir, fstype, blocksize, fslabel, rmmountdir=True, skipformat = False, fsopts = None, fsuuid = None):
648         self.__check_btrfs()
649         DiskMount.__init__(self, disk, mountdir, fstype, rmmountdir)
650         self.blocksize = blocksize
651         self.fslabel = fslabel.replace("/", "")
652         self.uuid  = fsuuid or None
653         self.skipformat = skipformat
654         self.fsopts = fsopts
655         self.blkidcmd = find_binary_path("blkid")
656         self.btrfsckcmd = find_binary_path("btrfsck")
657
658     def __check_btrfs(self):
659         found = False
660         """ Need to load btrfs module to mount it """
661         load_module("btrfs")
662         for line in open("/proc/filesystems").xreadlines():
663             if line.find("btrfs") > -1:
664                 found = True
665                 break
666         if not found:
667             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.")
668
669         # disable selinux, selinux will block write
670         if os.path.exists("/usr/sbin/setenforce"):
671             runner.show(["/usr/sbin/setenforce", "0"])
672
673     def __parse_field(self, output, field):
674         for line in output.split(" "):
675             if line.startswith(field + "="):
676                 return line[len(field) + 1:].strip().replace("\"", "")
677
678         raise KeyError("Failed to find field '%s' in output" % field)
679
680     def __format_filesystem(self):
681         if self.skipformat:
682             msger.debug("Skip filesystem format.")
683             return
684
685         msger.verbose("Formating %s filesystem on %s" % (self.fstype, self.disk.device))
686         rc = runner.show([self.mkfscmd, "-L", self.fslabel, self.disk.device])
687         if rc != 0:
688             raise MountError("Error creating %s filesystem on disk %s" % (self.fstype,self.disk.device))
689
690         self.uuid = self.__parse_field(runner.outs([self.blkidcmd, self.disk.device]), "UUID")
691
692     def __resize_filesystem(self, size = None):
693         current_size = os.stat(self.disk.lofile)[stat.ST_SIZE]
694
695         if size is None:
696             size = self.disk.size
697
698         if size == current_size:
699             return
700
701         if size > current_size:
702             self.disk.expand(size=size)
703
704         self.__fsck()
705         return size
706
707     def __create(self):
708         resize = False
709         if not self.disk.fixed() and self.disk.exists():
710             resize = True
711
712         self.disk.create()
713
714         if resize:
715             self.__resize_filesystem()
716         else:
717             self.__format_filesystem()
718
719     def mount(self, options = None, init_expand = False):
720         self.__create()
721         if init_expand:
722             expand_size = long(self.disk.size * 1.5)
723             msger.info("Initial partition size expanded : %ld -> %ld" % (self.disk.size, expand_size))
724             self.__resize_filesystem(expand_size)
725             self.disk.reread_size()
726         DiskMount.mount(self, options)
727
728     def __fsck(self):
729         msger.debug("Checking filesystem %s" % self.disk.lofile)
730         runner.quiet([self.btrfsckcmd, self.disk.lofile])
731
732     def __get_size_from_filesystem(self):
733         return self.disk.size
734
735     def __resize_to_minimal(self):
736         self.__fsck()
737
738         return self.__get_size_from_filesystem()
739
740     def resparse(self, size = None):
741         self.cleanup()
742         minsize = self.__resize_to_minimal()
743         self.disk.truncate(minsize)
744         self.__resize_filesystem(size)
745         return minsize
746
747 class DeviceMapperSnapshot(object):
748     def __init__(self, imgloop, cowloop):
749         self.imgloop = imgloop
750         self.cowloop = cowloop
751
752         self.__created = False
753         self.__name = None
754         self.dmsetupcmd = find_binary_path("dmsetup")
755
756         """Load dm_snapshot if it isn't loaded"""
757         load_module("dm_snapshot")
758
759     def get_path(self):
760         if self.__name is None:
761             return None
762         return os.path.join("/dev/mapper", self.__name)
763     path = property(get_path)
764
765     def create(self):
766         if self.__created:
767             return
768
769         self.imgloop.create()
770         self.cowloop.create()
771
772         self.__name = "imgcreate-%d-%d" % (os.getpid(),
773                                            random.randint(0, 2**16))
774
775         size = os.stat(self.imgloop.lofile)[stat.ST_SIZE]
776
777         table = "0 %d snapshot %s %s p 8" % (size / 512,
778                                              self.imgloop.device,
779                                              self.cowloop.device)
780
781         args = [self.dmsetupcmd, "create", self.__name, "--table", table]
782         if runner.show(args) != 0:
783             self.cowloop.cleanup()
784             self.imgloop.cleanup()
785             raise SnapshotError("Could not create snapshot device using: " + ' '.join(args))
786
787         self.__created = True
788
789     def remove(self, ignore_errors = False):
790         if not self.__created:
791             return
792
793         time.sleep(2)
794         rc = runner.show([self.dmsetupcmd, "remove", self.__name])
795         if not ignore_errors and rc != 0:
796             raise SnapshotError("Could not remove snapshot device")
797
798         self.__name = None
799         self.__created = False
800
801         self.cowloop.cleanup()
802         self.imgloop.cleanup()
803
804     def get_cow_used(self):
805         if not self.__created:
806             return 0
807
808         #
809         # dmsetup status on a snapshot returns e.g.
810         #   "0 8388608 snapshot 416/1048576"
811         # or, more generally:
812         #   "A B snapshot C/D"
813         # where C is the number of 512 byte sectors in use
814         #
815         out = runner.outs([self.dmsetupcmd, "status", self.__name])
816         try:
817             return int((out.split()[3]).split('/')[0]) * 512
818         except ValueError:
819             raise SnapshotError("Failed to parse dmsetup status: " + out)
820
821 def create_image_minimizer(path, image, minimal_size):
822     """
823     Builds a copy-on-write image which can be used to
824     create a device-mapper snapshot of an image where
825     the image's filesystem is as small as possible
826
827     The steps taken are:
828       1) Create a sparse COW
829       2) Loopback mount the image and the COW
830       3) Create a device-mapper snapshot of the image
831          using the COW
832       4) Resize the filesystem to the minimal size
833       5) Determine the amount of space used in the COW
834       6) Restroy the device-mapper snapshot
835       7) Truncate the COW, removing unused space
836       8) Create a squashfs of the COW
837     """
838     imgloop = LoopbackDisk(image, None) # Passing bogus size - doesn't matter
839
840     cowloop = SparseLoopbackDisk(os.path.join(os.path.dirname(path), "osmin"),
841                                  64L * 1024L * 1024L)
842
843     snapshot = DeviceMapperSnapshot(imgloop, cowloop)
844
845     try:
846         snapshot.create()
847
848         resize2fs(snapshot.path, minimal_size)
849
850         cow_used = snapshot.get_cow_used()
851     finally:
852         snapshot.remove(ignore_errors = (not sys.exc_info()[0] is None))
853
854     cowloop.truncate(cow_used)
855
856     mksquashfs(cowloop.lofile, path)
857
858     os.unlink(cowloop.lofile)
859
860 def load_module(module):
861     found = False
862     for line in open('/proc/modules').xreadlines():
863         if line.startswith("%s " % module):
864             found = True
865             break
866     if not found:
867         msger.info("Loading %s..." % module)
868         runner.quiet(['modprobe', module])
869
870 class LoopDevice(object):
871     def __init__(self, loopid=None):
872         self.device = None
873         self.loopid = loopid
874         self.created = False
875         self.kpartxcmd = find_binary_path("kpartx")
876         self.losetupcmd = find_binary_path("losetup")
877
878     def register(self, device):
879         self.device = device
880         self.loopid = None
881         #self.created = True
882
883     def reg_atexit(self):
884         import atexit
885         atexit.register(self.close)
886
887     def _genloopid(self):
888         import glob
889         if not glob.glob("/dev/loop[0-9]*"):
890             return 10
891
892         fint = lambda x: x[9:].isdigit() and int(x[9:]) or 0
893         maxid = 1 + max(filter(lambda x: x<100,
894                                map(fint, glob.glob("/dev/loop[0-9]*"))))
895         if maxid < 10: maxid = 10
896         if maxid >= 100: raise
897         return maxid
898
899     def _kpseek(self, device):
900         rc, out = runner.runtool([self.kpartxcmd, '-l', '-v', device])
901         if rc != 0:
902             raise MountError("Can't query dm snapshot on %s" % device)
903         for line in out.splitlines():
904             if line and line.startswith("loop"):
905                 return True
906         return False
907
908     def _loseek(self, device):
909         import re
910         rc, out = runner.runtool([self.losetupcmd, '-a'])
911         if rc != 0:
912             raise MountError("Failed to run 'losetup -a'")
913         for line in out.splitlines():
914             m = re.match("([^:]+): .*", line)
915             if m and m.group(1) == device:
916                 return True
917         return False
918
919     def create(self):
920         if not self.created:
921             if not self.loopid:
922                 self.loopid = self._genloopid()
923             self.device = "/dev/loop%d" % self.loopid
924             if os.path.exists(self.device):
925                 if self._loseek(self.device):
926                     raise MountError("Device busy: %s" % self.device)
927                 else:
928                     self.created = True
929                     return
930
931             mknod = find_binary_path('mknod')
932             rc = runner.show([mknod, '-m664', self.device, 'b', '7', str(self.loopid)])
933             if rc != 0:
934                 raise MountError("Failed to create device %s" % self.device)
935             else:
936                 self.created = True
937
938     def close(self):
939         if self.created:
940             try:
941                 self.cleanup()
942                 self.device = None
943             except MountError, e:
944                 raise CreatorError("%s" % e)
945
946     def cleanup(self):
947
948         if self.device is None:
949             return
950
951
952         if self._kpseek(self.device):
953             runner.quiet([self.kpartxcmd, "-d", self.device])
954         if self._loseek(self.device):
955             runner.quiet([self.losetupcmd, "-d", self.device])
956         # FIXME: should sleep a while between two loseek
957         if self._loseek(self.device):
958             msger.warning("Can't cleanup loop device %s" % self.device)
959         elif self.loopid:
960             os.unlink(self.device)
961
962 DEVICE_PIDFILE_DIR = "/var/tmp/mic/device"
963 DEVICE_LOCKFILE = "/var/lock/__mic_loopdev.lock"
964
965 def get_loop_device(losetupcmd, lofile):
966     global DEVICE_PIDFILE_DIR
967     global DEVICE_LOCKFILE
968
969     import fcntl
970     makedirs(os.path.dirname(DEVICE_LOCKFILE))
971     fp = open(DEVICE_LOCKFILE, 'w')
972     fcntl.flock(fp, fcntl.LOCK_EX)
973     try:
974         loopdev = None
975         devinst = LoopDevice()
976
977         # clean up left loop device first
978         clean_loop_devices()
979
980         # provide an avaible loop device
981         rc, out = runner.runtool([losetupcmd, "--find"])
982         if rc == 0 and out:
983             loopdev = out.split()[0]
984             devinst.register(loopdev)
985         if not loopdev or not os.path.exists(loopdev):
986             devinst.create()
987             loopdev = devinst.device
988
989         # setup a loop device for image file
990         rc = runner.show([losetupcmd, loopdev, lofile])
991         if rc != 0:
992             raise MountError("Failed to setup loop device for '%s'" % lofile)
993
994         devinst.reg_atexit()
995
996         # try to save device and pid
997         makedirs(DEVICE_PIDFILE_DIR)
998         pidfile = os.path.join(DEVICE_PIDFILE_DIR, os.path.basename(loopdev))
999         if os.path.exists(pidfile):
1000             os.unlink(pidfile)
1001         with open(pidfile, 'w') as wf:
1002             wf.write(str(os.getpid()))
1003
1004     except MountError, err:
1005         raise CreatorError("%s" % str(err))
1006     except:
1007         raise
1008     finally:
1009         try:
1010             fcntl.flock(fp, fcntl.LOCK_UN)
1011             fp.close()
1012         except:
1013             pass
1014
1015     return loopdev
1016
1017 def clean_loop_devices(piddir=DEVICE_PIDFILE_DIR):
1018     if not os.path.exists(piddir) or not os.path.isdir(piddir):
1019         return
1020
1021     for loopdev in os.listdir(piddir):
1022         pidfile = os.path.join(piddir, loopdev)
1023         try:
1024             with open(pidfile, 'r') as rf:
1025                 devpid = int(rf.read())
1026         except:
1027             devpid = None
1028
1029         # if the process using this device is alive, skip it
1030         if not devpid or os.path.exists(os.path.join('/proc', str(devpid))):
1031             continue
1032
1033         # try to clean it up
1034         try:
1035             devinst = LoopDevice()
1036             devinst.register(os.path.join('/dev', loopdev))
1037             devinst.cleanup()
1038             os.unlink(pidfile)
1039         except:
1040             pass
1041