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