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